CVE-2023-35933: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value: OPenFGA is an open source authorization/permission engine built for developers. OpenFGA versions v1.1.0 and prior are vulnerable to a DoS attack when Check and ListObjects calls are executed against authorization models that contain circular relationship definitions. Users are affected by this vulnerability if they are using OpenFGA v1.1.0 or earlier, and if you are executing `Check` or `ListObjects` calls against a vulnerable authorization model. Users are advised to upgrade to version 1.1.1. There are no known workarounds for this vulnerability. Users that do not have circular relationships in their models are not affected.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 7.5
Published: 2023-06-26T20:15Z
Repository: https://github.com/openfga/openfga
Language: Go
Files changed: cmd/validatemodels/validate_models.go, pkg/server/commands/expand.go, pkg/server/commands/write_authzmodel.go, pkg/server/server.go, pkg/server/server_test.go, pkg/server/test/connected_objects.go, pkg/server/test/expand.go, pkg/server/test/list_objects.go, pkg/server/test/server.go, pkg/server/test/write_authzmodel.go, pkg/typesystem/typesystem.go, pkg/typesystem/typesystem_test.go
Commit date: 2023-06-26 12:12:07 -0600
Commit message: Merge pull request from GHSA-hr9r-8phq-5x8j

* fix: validate models with no entrypoints correctly

* chore: fix map clone on union case

* fix: clone visited map for base and subtract set in exclusion

* fix: tidy up godoc and resolve issues with tests

* test: drop duplicate test cases

* test: add another valid model to the WriteAuthorizationModel test cases

* fix: add more model validations on empty type name and relations

* fix: add in model validation for check, list object and expand

* chore: fix linter warnings

* chore: fix usage of NewAndValidate

---------

Co-authored-by: Adrian Tam <adrian.tam@okta.com>
Diff: diff --git a/cmd/validatemodels/validate_models.go b/cmd/validatemodels/validate_models.go
index 26d8bcf..c930db5 100644
--- a/cmd/validatemodels/validate_models.go
+++ b/cmd/validatemodels/validate_models.go
@@ -124,11 +124,11 @@ func ValidateAllAuthorizationModels(ctx context.Context, db storage.OpenFGADatas
 					return nil, fmt.Errorf("error reading authorization models: %w", err)
 				}
 
 				// validate each model
 				for _, model := range models {
-					_, err := typesystem.NewAndValidate(model)
+					_, err := typesystem.NewAndValidate(context.Background(), model)
 
 					validationResult := validationResult{
 						StoreID:       store.Id,
 						ModelID:       model.Id,
 						IsLatestModel: model.Id == latestModelID,
diff --git a/pkg/server/commands/expand.go b/pkg/server/commands/expand.go
index 92e93fb..118b3d0 100644
--- a/pkg/server/commands/expand.go
+++ b/pkg/server/commands/expand.go
@@ -49,11 +49,14 @@ func (q *ExpandQuery) Execute(ctx context.Context, req *openfgapb.ExpandRequest)
 
 	if !typesystem.IsSchemaVersionSupported(model.GetSchemaVersion()) {
 		return nil, serverErrors.ValidationError(typesystem.ErrInvalidSchemaVersion)
 	}
 
-	typesys := typesystem.New(model)
+	typesys, err := typesystem.NewAndValidate(ctx, model)
+	if err != nil {
+		return nil, serverErrors.ValidationError(typesystem.ErrInvalidModel)
+	}
 
 	if err = validation.ValidateObject(typesys, tk); err != nil {
 		return nil, serverErrors.ValidationError(err)
 	}
 
diff --git a/pkg/server/commands/write_authzmodel.go b/pkg/server/commands/write_authzmodel.go
index 5a54ffb..a881132 100644
--- a/pkg/server/commands/write_authzmodel.go
+++ b/pkg/server/commands/write_authzmodel.go
@@ -43,11 +43,11 @@ func (w *WriteAuthorizationModelCommand) Execute(ctx context.Context, req *openf
 		Id:              ulid.Make().String(),
 		SchemaVersion:   req.GetSchemaVersion(),
 		TypeDefinitions: req.GetTypeDefinitions(),
 	}
 
-	_, err := typesystem.NewAndValidate(model)
+	_, err := typesystem.NewAndValidate(ctx, model)
 	if err != nil {
 		return nil, serverErrors.InvalidAuthorizationModelInput(err)
 	}
 
 	err = w.backend.WriteAuthorizationModel(ctx, req.GetStoreId(), model)
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 0ced5b1..31acaa0 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -103,11 +103,14 @@ func (s *Server) ListObjects(ctx context.Context, req *openfgapb.ListObjectsRequ
 		}
 
 		return nil, err
 	}
 
-	typesys := typesystem.New(model)
+	typesys, err := typesystem.NewAndValidate(ctx, model)
+	if err != nil {
+		return nil, serverErrors.ValidationError(typesystem.ErrInvalidModel)
+	}
 
 	ctx = typesystem.ContextWithTypesystem(ctx, typesys)
 
 	q := &commands.ListObjectsQuery{
 		Datastore:             s.datastore,
@@ -149,11 +152,14 @@ func (s *Server) StreamedListObjects(req *openfgapb.StreamedListObjectsRequest,
 		}
 
 		return serverErrors.HandleError("", err)
 	}
 
-	typesys := typesystem.New(model)
+	typesys, err := typesystem.NewAndValidate(ctx, model)
+	if err != nil {
+		return serverErrors.ValidationError(typesystem.ErrInvalidModel)
+	}
 
 	ctx = typesystem.ContextWithTypesystem(ctx, typesys)
 
 	q := &commands.ListObjectsQuery{
 		Datastore:             s.datastore,
@@ -235,11 +241,14 @@ func (s *Server) Check(ctx context.Context, req *openfgapb.CheckRequest) (*openf
 
 	if !typesystem.IsSchemaVersionSupported(model.GetSchemaVersion()) {
 		return nil, serverErrors.ValidationError(typesystem.ErrInvalidSchemaVersion)
 	}
 
-	typesys := typesystem.New(model)
+	typesys, err := typesystem.NewAndValidate(ctx, model)
+	if err != nil {
+		return nil, serverErrors.ValidationError(typesystem.ErrInvalidModel)
+	}
 
 	if err := validation.ValidateUserObjectRelation(typesys, tk); err != nil {
 		return nil, serverErrors.ValidationError(err)
 	}
 
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 2549926..a54a854 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -173,11 +173,11 @@ func TestCheckDoesNotThrowBecauseDirectTupleWasFound(t *testing.T) {
 	s := New(&Dependencies{
 		Datastore: mockDatastore,
 		Logger:    logger.NewNoopLogger(),
 		Transport: gateway.NewNoopTransport(),
 	}, &Config{
-		ResolveNodeLimit: 25,
+		ResolveNodeLimit: test.DefaultResolveNodeLimit,
 	})
 
 	checkResponse, err := s.Check(ctx, &openfgapb.CheckRequest{
 		StoreId:              storeID,
 		TupleKey:             tk,
@@ -185,10 +185,97 @@ func TestCheckDoesNotThrowBecauseDirectTupleWasFound(t *testing.T) {
 	})
 	require.NoError(t, err)
 	require.Equal(t, true, checkResponse.Allowed)
 }
 
+func TestOperationsWithInvalidModel(t *testing.T) {
+	ctx := context.Background()
+	storeID := ulid.Make().String()
+	modelID := ulid.Make().String()
+
+	// The model is invalid
+	typedefs := parser.MustParse(`
+	type user
+
+	type repo
+	  relations
+        define admin: [user] as self
+	    define r1: [user] as self and r2 and r3
+	    define r2: [user] as self and r1 and r3
+	    define r3: [user] as self and r1 and r2
+	`)
+
+	tk := tuple.NewTupleKey("repo:openfga", "r1", "user:anne")
+	mockController := gomock.NewController(t)
+	defer mockController.Finish()
+
+	mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController)
+
+	mockDatastore.EXPECT().
+		ReadAuthorizationModel(gomock.Any(), storeID, modelID).
+		AnyTimes().
+		Return(&openfgapb.AuthorizationModel{
+			SchemaVersion:   typesystem.SchemaVersion1_1,
+			TypeDefinitions: typedefs,
+		}, nil)
+
+	// the model is error and err should return
+
+	s := New(&Dependencies{
+		Datastore: mockDatastore,
+		Logger:    logger.NewNoopLogger(),
+		Transport: gateway.NewNoopTransport(),
+	}, &Config{
+		ResolveNodeLimit: test.DefaultResolveNodeLimit,
+	})
+
+	_, err := s.Check(ctx, &openfgapb.CheckRequest{
+		StoreId:              storeID,
+		TupleKey:             tk,
+		AuthorizationModelId: modelID,
+	})
+	require.Error(t, err)
+	e, ok := status.FromError(err)
+	require.True(t, ok)
+	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())
+
+	_, err = s.ListObjects(ctx, &openfgapb.ListObjectsRequest{
+		StoreId:              storeID,
+		AuthorizationModelId: modelID,
+		Type:                 "repo",
+		Relation:             "r1",
+		User:                 "user:anne",
+	})
+	require.Error(t, err)
+	e, ok = status.FromError(err)
+	require.True(t, ok)
+	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())
+
+	err = s.StreamedListObjects(&openfgapb.StreamedListObjectsRequest{
+		StoreId:              storeID,
+		AuthorizationModelId: modelID,
+		Type:                 "repo",
+		Relation:             "r1",
+		User:                 "user:anne",
+	}, NewMockStreamServer())
+	require.Error(t, err)
+	e, ok = status.FromError(err)
+	require.True(t, ok)
+	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())
+
+	_, err = s.Expand(ctx, &openfgapb.ExpandRequest{
+		StoreId:              storeID,
+		AuthorizationModelId: modelID,
+		TupleKey:             tk,
+	})
+	require.Error(t, err)
+	e, ok = status.FromError(err)
+	require.True(t, ok)
+	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())
+
+}
+
 func TestShortestPathToSolutionWins(t *testing.T) {
 	ctx := context.Background()
 
 	storeID := ulid.Make().String()
 	modelID := ulid.Make().String()
@@ -243,11 +330,11 @@ func TestShortestPathToSolutionWins(t *testing.T) {
 	s := New(&Dependencies{
 		Datastore: mockDatastore,
 		Logger:    logger.NewNoopLogger(),
 		Transport: gateway.NewNoopTransport(),
 	}, &Config{
-		ResolveNodeLimit: 25,
+		ResolveNodeLimit: test.DefaultResolveNodeLimit,
 	})
 
 	start := time.Now()
 	checkResponse, err := s.Check(ctx, &openfgapb.CheckRequest{
 		StoreId:              storeID,
@@ -378,11 +465,11 @@ func BenchmarkListObjectsNoRaceCondition(b *testing.B) {
 	s := New(&Dependencies{
 		Datastore: mockDatastore,
 		Transport: transport,
 		Logger:    logger,
 	}, &Config{
-		ResolveNodeLimit:      25,
+		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
 		ListObjectsDeadline:   5 * time.Second,
 		ListObjectsMaxResults: 1000,
 	})
 
 	b.ResetTimer()
@@ -438,11 +525,11 @@ func TestListObjects_Unoptimized_UnhappyPaths(t *testing.T) {
 	s := New(&Dependencies{
 		Datastore: mockDatastore,
 		Transport: transport,
 		Logger:    logger,
 	}, &Config{
-		ResolveNodeLimit:      25,
+		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
 		ListObjectsDeadline:   5 * time.Second,
 		ListObjectsMaxResults: 1000,
 	})
 
 	t.Run("error_listing_objects_from_storage_in_non-streaming_version", func(t *testing.T) {
@@ -519,11 +606,11 @@ func TestListObjects_UnhappyPaths(t *testing.T) {
 	s := New(&Dependencies{
 		Datastore: mockDatastore,
 		Transport: transport,
 		Logger:    logger,
 	}, &Config{
-		ResolveNodeLimit:      25,
+		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
 		ListObjectsDeadline:   5 * time.Second,
 		ListObjectsMaxResults: 1000,
 	})
 
 	t.Run("error_listing_objects_from_storage_in_non-streaming_version", func(t *testing.T) {
@@ -585,11 +672,11 @@ func TestAuthorizationModelInvalidSchemaVersion(t *testing.T) {
 	s := Server{
 		datastore: mockDatastore,
 		transport: transport,
 		logger:    logger,
 		config: &Config{
-			ResolveNodeLimit:      25,
+			ResolveNodeLimit:      test.DefaultResolveNodeLimit,
 			ListObjectsDeadline:   5 * time.Second,
 			ListObjectsMaxResults: 1000,
 		},
 	}
 
diff --git a/pkg/server/test/connected_objects.go b/pkg/server/test/connected_objects.go
index 49beccb..7b6003d 100644
--- a/pkg/server/test/connected_objects.go
+++ b/pkg/server/test/connected_objects.go
@@ -906,11 +906,11 @@ func ConnectedObjectsTest(t *testing.T, ds storage.OpenFGADatastore) {
 
 			err = ds.Write(ctx, store, nil, test.tuples)
 			require.NoError(err)
 
 			if test.resolveNodeLimit == 0 {
-				test.resolveNodeLimit = defaultResolveNodeLimit
+				test.resolveNodeLimit = DefaultResolveNodeLimit
 			}
 
 			connectedObjectsCmd := commands.ConnectedObjectsCommand{
 				Datastore:        ds,
 				ResolveNodeLimit: test.resolveNodeLimit,
diff --git a/pkg/server/test/expand.go b/pkg/server/test/expand.go
index a76528e..4c673e8 100644
--- a/pkg/server/test/expand.go
+++ b/pkg/server/test/expand.go
@@ -88,10 +88,13 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
 			name: "1.1_computed_userset",
 			model: &openfgapb.AuthorizationModel{
 				Id:            ulid.Make().String(),
 				SchemaVersion: typesystem.SchemaVersion1_1,
 				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "user",
+					},
 					{
 						Type: "repo",
 						Relations: map[string]*openfgapb.Userset{
 							"admin":  typesystem.This(),
 							"writer": typesystem.ComputedUserset("admin"),
@@ -809,10 +812,13 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
 			name: "1.1_Tuple_involving_userset_that_is_not_involved_in_TTU_rewrite",
 			model: &openfgapb.AuthorizationModel{
 				Id:            ulid.Make().String(),
 				SchemaVersion: typesystem.SchemaVersion1_1,
 				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "user",
+					},
 					{
 						Type: "document",
 						Relations: map[string]*openfgapb.Userset{
 							"parent": typesystem.This(),
 							"editor": typesystem.This(),
@@ -822,310 +828,37 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
 								"parent": {
 									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
 										typesystem.DirectRelationReference("document", "editor"),
 									},
 								},
-							},
-						},
-					},
-				},
-			},
-			tuples: []*openfgapb.TupleKey{
-				tuple.NewTupleKey("document:1", "parent", "document:2#editor"),
-			},
-			request: &openfgapb.ExpandRequest{
-				TupleKey: tuple.NewTupleKey("document:1", "parent", ""),
-			},
-			expected: &openfgapb.ExpandResponse{
-				Tree: &openfgapb.UsersetTree{
-					Root: &openfgapb.UsersetTree_Node{
-						Name: "document:1#parent",
-						Value: &openfgapb.UsersetTree_Node_Leaf{
-							Leaf: &openfgapb.UsersetTree_Leaf{
-								Value: &openfgapb.UsersetTree_Leaf_Users{
-									Users: &openfgapb.UsersetTree_Users{
-										Users: []string{"document:2#editor"},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		{
-			name: "1.1_TupleToUserset_involving_wildcard_is_skipped",
-			model: &openfgapb.AuthorizationModel{
-				Id:            ulid.Make().String(),
-				SchemaVersion: typesystem.SchemaVersion1_1,
-				TypeDefinitions: []*openfgapb.TypeDefinition{
-					{
-						Type: "user",
-					},
-					{
-						Type: "document",
-						Relations: map[string]*openfgapb.Userset{
-							"parent": typesystem.This(),
-							"viewer": typesystem.Union(
-								typesystem.This(),
-								typesystem.TupleToUserset("parent", "viewer"),
-							),
-						},
-						Metadata: &openfgapb.Metadata{
-							Relations: map[string]*openfgapb.RelationMetadata{
-								"parent": {
+								"editor": {
 									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
-										typesystem.WildcardRelationReference("user"),
 										{
 											Type: "user",
 										},
 									},
 								},
 							},
 						},
 					},
 				},
 			},
-			tuples: []*openfgapb.TupleKey{
-				tuple.NewTupleKey("document:1", "parent", "user:*"),
-				tuple.NewTupleKey("document:X", "viewer", "user:jon"),
-			},
-			request: &openfgapb.ExpandRequest{
-				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
-			},
-			expected: &openfgapb.ExpandResponse{
-				Tree: &openfgapb.UsersetTree{
-					Root: &openfgapb.UsersetTree_Node{
-						Name: "document:1#viewer",
-						Value: &openfgapb.UsersetTree_Node_Union{
-							Union: &openfgapb.UsersetTree_Nodes{
-								Nodes: []*openfgapb.UsersetTree_Node{
-									{
-										Name: "document:1#viewer",
-										Value: &openfgapb.UsersetTree_Node_Leaf{
-											Leaf: &openfgapb.UsersetTree_Leaf{
-												Value: &openfgapb.UsersetTree_Leaf_Users{},
-											},
-										},
-									},
-									{
-										Name: "document:1#viewer",
-										Value: &openfgapb.UsersetTree_Node_Leaf{
-											Leaf: &openfgapb.UsersetTree_Leaf{
-												Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
-													TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
-														Tupleset: "document:1#parent",
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		{
-			name: "1.1_Tuple_involving_userset_skipped_if_it_is_referenced_in_a_TTU_rewrite",
-			model: &openfgapb.AuthorizationModel{
-				Id:            ulid.Make().String(),
-				SchemaVersion: typesystem.SchemaVersion1_1,
-				TypeDefinitions: []*openfgapb.TypeDefinition{
-					{
-						Type: "folder",
-						Relations: map[string]*openfgapb.Userset{
-							"viewer": typesystem.This(),
-						},
-					},
-					{
-						Type: "document",
-						Relations: map[string]*openfgapb.Userset{
-							"parent": typesystem.This(),
-							"editor": typesystem.This(),
-							"viewer": typesystem.TupleToUserset("parent", "viewer"),
-						},
-						Metadata: &openfgapb.Metadata{
-							Relations: map[string]*openfgapb.RelationMetadata{
-								"parent": {
-									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
-										typesystem.DirectRelationReference("document", "editor"),
-									},
-								},
-							},
-						},
-					},
-				},
-			},
 			tuples: []*openfgapb.TupleKey{
 				tuple.NewTupleKey("document:1", "parent", "document:2#editor"),
 			},
 			request: &openfgapb.ExpandRequest{
-				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
+				TupleKey: tuple.NewTupleKey("document:1", "parent", ""),
 			},
 			expected: &openfgapb.ExpandResponse{
 				Tree: &openfgapb.UsersetTree{
 					Root: &openfgapb.UsersetTree_Node{
-						Name: "document:1#viewer",
+						Name: "document:1#parent",
 						Value: &openfgapb.UsersetTree_Node_Leaf{
 							Leaf: &openfgapb.UsersetTree_Leaf{
-								Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
-									TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
-										Tupleset: "document:1#parent",
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		{
-			name: "1.1_Tuple_involving_userset_skipped_if_same_ComputedUserset_involved_in_TTU_rewrite",
-			model: &openfgapb.AuthorizationModel{
-				Id:            ulid.Make().String(),
-				SchemaVersion: typesystem.SchemaVersion1_1,
-				TypeDefinitions: []*openfgapb.TypeDefinition{
-					{
-						Type: "user",
-					},
-					{
-						Type: "document",
-						Relations: map[string]*openfgapb.Userset{
-							"parent": typesystem.This(),
-							"viewer": typesystem.Union(
-								typesystem.This(),
-								typesystem.TupleToUserset("parent", "viewer"),
-							),
-						},
-						Metadata: &openfgapb.Metadata{
-							Relations: map[string]*openfgapb.RelationMetadata{
-								"viewer": {
-									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
-										{Type: "user"},
-									},
-								},
-								"parent": {
-									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
-										typesystem.DirectRelationReference("document", "viewer"),
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			tuples: []*openfgapb.TupleKey{
-				tuple.NewTupleKey("document:1", "parent", "document:2#viewer"),
-				tuple.NewTupleKey("document:2", "viewer", "user:jon"),
-			},
-			request: &openfgapb.ExpandRequest{
-				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
-			},
-			expected: &openfgapb.ExpandResponse{
-				Tree: &openfgapb.UsersetTree{
-					Root: &openfgapb.UsersetTree_Node{
-						Name: "document:1#viewer",
-						Value: &openfgapb.UsersetTree_Node_Union{
-							Union: &openfgapb.UsersetTree_Nodes{
-								Nodes: []*openfgapb.UsersetTree_Node{
-									{
-										Name: "document:1#viewer",
-										Value: &openfgapb.UsersetTree_Node_Leaf{
-											Leaf: &openfgapb.UsersetTree_Leaf{
-												Value: &openfgapb.UsersetTree_Leaf_Users{},
-											},
-										},
-									},
-									{
-										Name: "document:1#viewer",
-										Value: &openfgapb.UsersetTree_Node_Leaf{
-											Leaf: &openfgapb.UsersetTree_Leaf{
-												Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
-													TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
-														Tupleset: "document:1#parent",
-													},
-												},
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		{
-			name: "1.1_Tupleset_relation_involving_rewrite_skipped",
-			model: &openfgapb.AuthorizationModel{
-				Id:            ulid.Make().String(),
-				SchemaVersion: typesystem.SchemaVersion1_1,
-				TypeDefinitions: []*openfgapb.TypeDefinition{
-					{
-						Type: "user",
-					},
-					{
-						Type: "document",
-						Relations: map[string]*openfgapb.Userset{
-							"parent": typesystem.ComputedUserset("editor"),
-							"editor": typesystem.This(),
-							"viewer": typesystem.Union(
-								typesystem.This(), typesystem.TupleToUserset("parent", "viewer"),
-							),
-						},
-						Metadata: &openfgapb.Metadata{
-							Relations: map[string]*openfgapb.RelationMetadata{
-								"editor": {
-									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
-										{Type: "document"},
-									},
-								},
-								"viewer": {
-									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
-										{Type: "user"},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			tuples: []*openfgapb.TupleKey{
-				tuple.NewTupleKey("document:1", "editor", "document:2"),
-				tuple.NewTupleKey("document:2", "viewer", "user:jon"),
-			},
-			request: &openfgapb.ExpandRequest{
-				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
-			},
-			expected: &openfgapb.ExpandResponse{
-				Tree: &openfgapb.UsersetTree{
-					Root: &openfgapb.UsersetTree_Node{
-						Name: "document:1#viewer",
-						Value: &openfgapb.UsersetTree_Node_Union{
-							Union: &openfgapb.UsersetTree_Nodes{
-								Nodes: []*openfgapb.UsersetTree_Node{
-									{
-										Name: "document:1#viewer",
-										Value: &openfgapb.UsersetTree_Node_Leaf{
-											Leaf: &openfgapb.UsersetTree_Leaf{
-												Value: &openfgapb.UsersetTree_Leaf_Users{},
-											},
-										},
-									},
-									{
-										Name: "document:1#viewer",
-										Value: &openfgapb.UsersetTree_Node_Leaf{
-											Leaf: &openfgapb.UsersetTree_Leaf{
-												Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
-													TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
-														Tupleset: "document:1#parent",
-													},
-												},
-											},
-										},
+								Value: &openfgapb.UsersetTree_Leaf_Users{
+									Users: &openfgapb.UsersetTree_Users{
+										Users: []string{"document:2#editor"},
 									},
 								},
 							},
 						},
 					},
diff --git a/pkg/server/test/list_objects.go b/pkg/server/test/list_objects.go
index c7a47eb..703b82b 100644
--- a/pkg/server/test/list_objects.go
+++ b/pkg/server/test/list_objects.go
@@ -201,11 +201,11 @@ func TestListObjectsRespectsMaxResults(t *testing.T, ds storage.OpenFGADatastore
 			listObjectsQuery := &commands.ListObjectsQuery{
 				Datastore:             datastore,
 				Logger:                logger.NewNoopLogger(),
 				ListObjectsDeadline:   listObjectsDeadline,
 				ListObjectsMaxResults: test.maxResults,
-				ResolveNodeLimit:      defaultResolveNodeLimit,
+				ResolveNodeLimit:      DefaultResolveNodeLimit,
 			}
 			typesys := typesystem.New(model)
 			ctx = typesystem.ContextWithTypesystem(ctx, typesys)
 
 			// assertions
@@ -311,11 +311,11 @@ func BenchmarkListObjectsWithReverseExpand(b *testing.B, ds storage.OpenFGADatas
 	}
 
 	listObjectsQuery := commands.ListObjectsQuery{
 		Datastore:        ds,
 		Logger:           logger.NewNoopLogger(),
-		ResolveNodeLimit: defaultResolveNodeLimit,
+		ResolveNodeLimit: DefaultResolveNodeLimit,
 	}
 
 	var r *openfgapb.ListObjectsResponse
 
 	ctx = typesystem.ContextWithTypesystem(ctx, typesystem.New(model))
@@ -377,11 +377,11 @@ func BenchmarkListObjectsWithConcurrentChecks(b *testing.B, ds storage.OpenFGADa
 	}
 
 	listObjectsQuery := commands.ListObjectsQuery{
 		Datastore:        ds,
 		Logger:           logger.NewNoopLogger(),
-		ResolveNodeLimit: defaultResolveNodeLimit,
+		ResolveNodeLimit: DefaultResolveNodeLimit,
 	}
 
 	var r *openfgapb.ListObjectsResponse
 
 	ctx = typesystem.ContextWithTypesystem(ctx, typesystem.New(model))
diff --git a/pkg/server/test/server.go b/pkg/server/test/server.go
index 992e3d1..e64d569 100644
--- a/pkg/server/test/server.go
+++ b/pkg/server/test/server.go
@@ -5,11 +5,11 @@ import (
 
 	"github.com/openfga/openfga/pkg/storage"
 )
 
 const (
-	defaultResolveNodeLimit = 25
+	DefaultResolveNodeLimit = 25
 )
 
 func RunAllTests(t *testing.T, ds storage.OpenFGADatastore) {
 	RunQueryTests(t, ds)
 	RunCommandTests(t, ds)
@@ -17,11 +17,10 @@ func RunAllTests(t *testing.T, ds storage.OpenFGADatastore) {
 
 func RunQueryTests(t *testing.T, ds storage.OpenFGADatastore) {
 	t.Run("TestReadAuthorizationModelQueryErrors", func(t *testing.T) { TestReadAuthorizationModelQueryErrors(t, ds) })
 	t.Run("TestSuccessfulReadAuthorizationModelQuery", func(t *testing.T) { TestSuccessfulReadAuthorizationModelQuery(t, ds) })
 	t.Run("TestReadAuthorizationModel", func(t *testing.T) { ReadAuthorizationModelTest(t, ds) })
-
 	t.Run("TestExpandQuery", func(t *testing.T) { TestExpandQuery(t, ds) })
 	t.Run("TestExpandQueryErrors", func(t *testing.T) { TestExpandQueryErrors(t, ds) })
 
 	t.Run("TestGetStoreQuery", func(t *testing.T) { TestGetStoreQuery(t, ds) })
 	t.Run("TestGetStoreSucceeds", func(t *testing.T) { TestGetStoreSucceeds(t, ds) })
diff --git a/pkg/server/test/write_authzmodel.go b/pkg/server/test/write_authzmodel.go
index 1def495..2e67af1 100644
--- a/pkg/server/test/write_authzmodel.go
+++ b/pkg/server/test/write_authzmodel.go
@@ -4,10 +4,11 @@ import (
 	"context"
 	"errors"
 	"fmt"
 	"testing"
 
+	parser "github.com/craigpastro/openfga-dsl-parser/v2"
 	"github.com/oklog/ulid/v2"
 	"github.com/openfga/openfga/pkg/logger"
 	"github.com/openfga/openfga/pkg/server/commands"
 	serverErrors "github.com/openfga/openfga/pkg/server/errors"
 	"github.com/openfga/openfga/pkg/storage"
@@ -133,10 +134,302 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 						},
 					},
 				},
 			},
 		},
+		{
+			name: "self_referencing_type_restriction_with_entrypoint",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define editor: [user] as self
+				    define viewer: [document#viewer] as self or editor
+				`),
+				SchemaVersion: typesystem.SchemaVersion1_1,
+			},
+		},
+		{
+			name: "self_referencing_type_restriction_without_entrypoint_1",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+				type document
+				  relations
+				    define viewer: [document#viewer] as self
+				`),
+				SchemaVersion: typesystem.SchemaVersion1_1,
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "viewer",
+				Cause:      typesystem.ErrCycle,
+			}),
+		},
+		{
+			name: "self_referencing_type_restriction_without_entrypoint_2",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+				type document
+				  relations
+				    define editor: [user] as self
+				    define viewer: [document#viewer] as self and editor
+				`),
+				SchemaVersion: typesystem.SchemaVersion1_1,
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "viewer",
+				Cause:      typesystem.ErrNoEntrypoints,
+			}),
+		},
+		{
+			name: "self_referencing_type_restriction_without_entrypoint_3",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+				type document
+				  relations
+				    define restricted: [user] as self
+				    define viewer: [document#viewer] as self but not restricted
+				`),
+				SchemaVersion: typesystem.SchemaVersion1_1,
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "viewer",
+				Cause:      typesystem.ErrCycle,
+			}),
+		},
+		{
+			name: "rewritten_relation_in_intersection_unresolvable",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define admin: [user] as self
+				    define action1 as admin and action2 and action3
+				    define action2 as admin and action1 and action3
+				    define action3 as admin and action1 and action2
+				`),
+				SchemaVersion: typesystem.SchemaVersion1_1,
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "action1",
+				Cause:      typesystem.ErrNoEntrypoints,
+			}),
+		},
+		{
+			name: "direct_relationship_with_entrypoint",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define viewer: [user] as self
+				`),
+			},
+		},
+		{
+			name: "computed_relationship_with_entrypoint",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define editor: [user] as self
+				    define viewer as editor
+				`),
+			},
+		},
+
+		{
+			name: "rewritten_relation_in_exclusion_unresolvable",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define admin: [user] as self
+				    define action1 as admin but not action2
+				    define action2 as admin but not action3
+				    define action3 as admin but not action1
+				`),
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "action1",
+				Cause:      typesystem.ErrNoEntrypoints,
+			}),
+		},
+		{
+			name: "no_entrypoint_3a",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define viewer: [document#viewer] as self and editor
+				    define editor: [user] as self
+				`),
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "viewer",
+				Cause:      typesystem.ErrNoEntrypoints,
+			}),
+		},
+
+		{
+			name: "no_entrypoint_3b",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define viewer: [document#viewer] as self but not editor
+				    define editor: [user] as self
+				`),
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "viewer",
+				Cause:      typesystem.ErrNoEntrypoints,
+			}),
+		},
+		{
+			name: "no_entrypoint_4",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type folder
+				  relations
+				    define parent: [document] as self
+				    define viewer as editor from parent
+
+				type document
+				  relations
+				    define parent: [folder] as self
+				    define editor as viewer
+				    define viewer as editor from parent
+				`),
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
+				ObjectType: "document",
+				Relation:   "editor",
+				Cause:      typesystem.ErrNoEntrypoints,
+			}),
+		},
+		{
+			name: "self_referencing_type_restriction_with_entrypoint_1",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define restricted: [user] as self
+				    define editor: [user] as self
+				    define viewer: [document#viewer] as self or editor
+				    define can_view as viewer but not restricted
+				    define can_view_actual as can_view
+				`),
+			},
+		},
+		{
+			name: "self_referencing_type_restriction_with_entrypoint_2",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define editor: [user] as self
+				    define viewer: [document#viewer] as self or editor
+				`),
+			},
+		},
+		{
+			name: "relation_with_union_of_ttu_rewrites",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: parser.MustParse(`
+				type user
+				type org
+				  relations
+				    define admin: [user] as self
+				    define member: [user] as self
+				type group
+				  relations
+				    define member: [user] as self
+				type feature
+				  relations
+				    define accessible as admin from subscriber_org or member from subscriber_group
+				    define subscriber_group: [group] as self
+				    define subscriber_org: [org] as self
+				`),
+			},
+		},
+		{
+			name: "type_name_is_empty_string",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "",
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				fmt.Errorf("the type name of a type definition cannot be an empty string"),
+			),
+		},
+		{
+			name: "relation_name_is_empty_string",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "user",
+						Relations: map[string]*openfgapb.Userset{
+							"": typesystem.This(),
+						},
+					},
+					{
+						Type: "other",
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				fmt.Errorf("type 'user' defines a relation with an empty string for a name"),
+			),
+		},
 	}
 
 	ctx := context.Background()
 	logger := logger.NewNoopLogger()
 
diff --git a/pkg/typesystem/typesystem.go b/pkg/typesystem/typesystem.go
index f8b68c7..ff36ad4 100644
--- a/pkg/typesystem/typesystem.go
+++ b/pkg/typesystem/typesystem.go
@@ -3,15 +3,20 @@ package typesystem
 import (
 	"context"
 	"errors"
 	"fmt"
 	"reflect"
+	"sort"
 
 	"github.com/openfga/openfga/pkg/tuple"
 	openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1"
+	"go.opentelemetry.io/otel"
+	"golang.org/x/exp/maps"
 )
 
+var tracer = otel.Tracer("openfga/pkg/typesystem")
+
 type ctxKey string
 
 const (
 	SchemaVersion1_0 string = "1.0"
 	SchemaVersion1_1 string = "1.1"
@@ -26,10 +31,11 @@ var (
 	ErrRelationUndefined     = errors.New("undefined relation")
 	ErrObjectTypeUndefined   = errors.New("undefined object type")
 	ErrInvalidUsersetRewrite = errors.New("invalid userset rewrite definition")
 	ErrReservedKeywords      = errors.New("self and this are reserved keywords")
 	ErrCycle                 = errors.New("an authorization model cannot contain a cycle")
+	ErrNoEntrypoints         = errors.New("no entrypoints defined")
 )
 
 func IsSchemaVersionSupported(version string) bool {
 	switch version {
 	case SchemaVersion1_1:
@@ -148,11 +154,13 @@ type TypeSystem struct {
 func New(model *openfgapb.AuthorizationModel) *TypeSystem {
 	tds := make(map[string]*openfgapb.TypeDefinition, len(model.GetTypeDefinitions()))
 	relations := make(map[string]map[string]*openfgapb.Relation, len(model.GetTypeDefinitions()))
 
 	for _, td := range model.GetTypeDefinitions() {
-		tds[td.GetType()] = td
+		typeName := td.GetType()
+
+		tds[typeName] = td
 		tdRelations := make(map[string]*openfgapb.Relation, len(td.GetRelations()))
 
 		for relation, rewrite := range td.GetRelations() {
 			r := &openfgapb.Relation{
 				Name:     relation,
@@ -164,11 +172,11 @@ func New(model *openfgapb.AuthorizationModel) *TypeSystem {
 				r.TypeInfo.DirectlyRelatedUserTypes = metadata.GetDirectlyRelatedUserTypes()
 			}
 
 			tdRelations[relation] = r
 		}
-		relations[td.GetType()] = tdRelations
+		relations[typeName] = tdRelations
 	}
 
 	return &TypeSystem{
 		modelID:         model.GetId(),
 		schemaVersion:   model.GetSchemaVersion(),
@@ -594,10 +602,174 @@ func (t *TypeSystem) relationInvolvesExclusion(objectType, relation string, visi
 	}
 
 	return false, nil
 }
 
+// hasEntrypoints recursively walks the rewrite definition for the given relation to determine if there is at least
+// one path in the rewrite rule that could relate to at least one concrete object type. If there is no such path that
+// could lead to at least one relationship with some object type, then false is returned along with an error indicating
+// no entrypoints were found. If at least one relationship with a specific object type is found while walking the rewrite,
+// then true is returned along with a nil error.
+func hasEntrypoints(
+	typedefs map[string]map[string]*openfgapb.Relation,
+	typeName, relationName string,
+	rewrite *openfgapb.Userset,
+	visitedRelations map[string]map[string]struct{},
+) (bool, error) {
+
+	v := maps.Clone(visitedRelations)
+
+	if val, ok := v[typeName]; ok {
+		val[relationName] = struct{}{}
+	} else {
+		v[typeName] = map[string]struct{}{
+			relationName: {},
+		}
+	}
+
+	relation, ok := typedefs[typeName][relationName]
+	if !ok {
+		return false, fmt.Errorf("undefined type definition for '%s#%s'", typeName, relationName)
+	}
+
+	switch rw := rewrite.Userset.(type) {
+	case *openfgapb.Userset_This:
+		for _, assignableType := range relation.GetTypeInfo().GetDirectlyRelatedUserTypes() {
+			if assignableType.GetRelationOrWildcard() == nil || assignableType.GetWildcard() != nil {
+				return true, nil
+			}
+
+			assignableTypeName := assignableType.GetType()
+			assignableRelationName := assignableType.GetRelation()
+
+			assignableRelation, ok := typedefs[assignableTypeName][assignableRelationName]
+			if !ok {
+				return false, fmt.Errorf("undefined type definition for '%s#%s'", assignableTypeName, assignableRelationName)
+			}
+
+			if _, ok := v[assignableTypeName][assignableRelationName]; ok {
+				continue
+			}
+
+			hasEntrypoint, err := hasEntrypoints(typedefs, assignableTypeName, assignableRelationName, assignableRelation.GetRewrite(), v)
+			if err != nil {
+				return false, err
+			}
+
+			if hasEntrypoint {
+				return true, nil
+			}
+		}
+
+		return false, nil
+	case *openfgapb.Userset_ComputedUserset:
+
+		computedRelationName := rw.ComputedUserset.GetRelation()
+		computedRelation, ok := typedefs[typeName][computedRelationName]
+		if !ok {
+			return false, fmt.Errorf("undefined type definition for '%s#%s'", typeName, computedRelationName)
+		}
+
+		if _, ok := v[typeName][computedRelationName]; ok {
+			return false, nil
+		}
+
+		hasEntrypoint, err := hasEntrypoints(typedefs, typeName, computedRelationName, computedRelation.GetRewrite(), v)
+		if err != nil {
+			return false, err
+		}
+
+		return hasEntrypoint, nil
+	case *openfgapb.Userset_TupleToUserset:
+		tuplesetRelationName := rw.TupleToUserset.GetTupleset().GetRelation()
+		computedRelationName := rw.TupleToUserset.ComputedUserset.GetRelation()
+
+		tuplesetRelation, ok := typedefs[typeName][tuplesetRelationName]
+		if !ok {
+			return false, fmt.Errorf("undefined type definition for '%s#%s'", typeName, tuplesetRelationName)
+		}
+
+		for _, assignableType := range tuplesetRelation.GetTypeInfo().GetDirectlyRelatedUserTypes() {
+			assignableTypeName := assignableType.GetType()
+
+			if assignableRelation, ok := typedefs[assignableTypeName][computedRelationName]; ok {
+				if _, ok := v[assignableTypeName][computedRelationName]; ok {
+					continue
+				}
+
+				hasEntrypoint, err := hasEntrypoints(typedefs, assignableTypeName, computedRelationName, assignableRelation.GetRewrite(), v)
+				if err != nil {
+					return false, err
+				}
+
+				if hasEntrypoint {
+					return true, nil
+				}
+			}
+		}
+
+		return false, nil
+
+	case *openfgapb.Userset_Union:
+
+		for _, child := range rw.Union.Child {
+
+			hasEntrypoints, err := hasEntrypoints(typedefs, typeName, relationName, child, maps.Clone(visitedRelations))
+			if err != nil {
+				return false, err
+			}
+
+			if hasEntrypoints {
+				return true, nil
+			}
+		}
+
+		return false, nil
+	case *openfgapb.Userset_Intersection:
+
+		for _, child := range rw.Intersection.Child {
+
+			// all of the children must have an entrypoint
+			hasEntrypoints, err := hasEntrypoints(typedefs, typeName, relationName, child, maps.Clone(visitedRelations))
+			if err != nil {
+				return false, err
+			}
+
+			if !hasEntrypoints {
+				return false, nil
+			}
+		}
+
+		return true, nil
+	case *openfgapb.Userset_Difference:
+
+		v := maps.Clone(visitedRelations)
+
+		hasEntrypoint, err := hasEntrypoints(typedefs, typeName, relationName, rw.Difference.GetBase(), v)
+		if err != nil {
+			return false, err
+		}
+
+		if !hasEntrypoint {
+			return false, nil
+		}
+
+		hasEntrypoint, err = hasEntrypoints(typedefs, typeName, relationName, rw.Difference.GetSubtract(), v)
+		if err != nil {
+			return false, err
+		}
+
+		if !hasEntrypoint {
+			return false, nil
+		}
+
+		return true, nil
+	}
+
+	return false, nil
+}
+
 // NewAndValidate is like New but also validates the model according to the following rules:
 //  1. Checks that the *TypeSystem have a valid schema version.
 //  2. For every rewrite the relations in the rewrite must:
 //     a. Be valid relations on the same type in the *TypeSystem (in cases of computedUserset)
 //     b. Be valid relations on another existing type (in cases of tupleToUserset)
@@ -608,11 +780,14 @@ func (t *TypeSystem) relationInvolvesExclusion(objectType, relation string, visi
 // validate the *TypeSystem according to the following rules:
 //  3. Every type restriction on a relation must be a valid type:
 //     a. For a type (e.g. user) this means checking that this type is in the *TypeSystem
 //     b. For a type#relation this means checking that this type with this relation is in the *TypeSystem
 //  4. Check that a relation is assignable if and only if it has a non-zero list of types
-func NewAndValidate(model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
+func NewAndValidate(ctx context.Context, model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
+	_, span := tracer.Start(ctx, "typesystem.NewAndValidate")
+	defer span.End()
+
 	t := New(model)
 	schemaVersion := t.GetSchemaVersion()
 
 	if !IsSchemaVersionSupported(schemaVersion) {
 		return nil, ErrInvalidSchemaVersion
@@ -624,14 +799,35 @@ func NewAndValidate(model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
 
 	if err := t.validateNames(); err != nil {
 		return nil, err
 	}
 
-	// Validate the userset rewrites
-	for _, td := range t.typeDefinitions {
-		for relation, rewrite := range td.GetRelations() {
-			err := t.isUsersetRewriteValid(td.GetType(), relation, rewrite)
+	typedefsMap := t.typeDefinitions
+
+	typeNames := make([]string, 0, len(typedefsMap))
+	for typeName := range typedefsMap {
+		typeNames = append(typeNames, typeName)
+	}
+
+	// range over the type definitions in sorted order to produce a deterministic outcome
+	sort.Strings(typeNames)
+
+	for _, typeName := range typeNames {
+		typedef := typedefsMap[typeName]
+
+		relationMap := typedef.GetRelations()
+		relationNames := make([]string, 0, len(relationMap))
+		for relationName := range relationMap {
+			relationNames = append(relationNames, relationName)
+		}
+
+		// range over the relations in sorted order to produce a deterministic outcome
+		sort.Strings(relationNames)
+
+		for _, relationName := range relationNames {
+
+			err := t.validateRelation(typeName, relationName, relationMap)
 			if err != nil {
 				return nil, err
 			}
 		}
 	}
@@ -642,17 +838,46 @@ func NewAndValidate(model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
 
 	if err := t.ensureNoCyclesInComputedRewrite(); err != nil {
 		return nil, err
 	}
 
-	if schemaVersion == SchemaVersion1_1 {
-		if err := t.validateRelationTypeRestrictions(); err != nil {
-			return nil, err
+	return t, nil
+}
+
+// validateRelation applies all of the validation rules to a relation definition in a model. A relation
+// must meet all of the rewrite validation, type restriction valdiation, and entrypoint validation criteria
+// for it to be valid. Otherrwise an error is returned.
+func (t *TypeSystem) validateRelation(typeName, relationName string, relationMap map[string]*openfgapb.Userset) error {
+
+	rewrite := relationMap[relationName]
+
+	err := t.isUsersetRewriteValid(typeName, relationName, rewrite)
+	if err != nil {
+		return err
+	}
+
+	err = t.validateTypeRestrictions(typeName, relationName)
+	if err != nil {
+		return err
+	}
+
+	visitedRelations := map[string]map[string]struct{}{}
+
+	hasEntrypoints, err := hasEntrypoints(t.relations, typeName, relationName, rewrite, visitedRelations)
+	if err != nil {
+		return err
+	}
+
+	if !hasEntrypoints {
+		return &InvalidRelationError{
+			ObjectType: typeName,
+			Relation:   relationName,
+			Cause:      fmt.Errorf("no entrypoints found for relation '%s#%s': %w", typeName, relationName, ErrNoEntrypoints),
 		}
 	}
 
-	return t, nil
+	return nil
 }
 
 func containsDuplicateType(model *openfgapb.AuthorizationModel) bool {
 	seen := make(map[string]struct{}, len(model.GetTypeDefinitions()))
 	for _, td := range model.GetTypeDefinitions() {
@@ -668,15 +893,24 @@ func containsDuplicateType(model *openfgapb.AuthorizationModel) bool {
 // validateNames ensures that a model doesn't have object types or relations
 // called "self" or "this"
 func (t *TypeSystem) validateNames() error {
 	for _, td := range t.typeDefinitions {
 		objectType := td.GetType()
+
+		if objectType == "" {
+			return fmt.Errorf("the type name of a type definition cannot be an empty string")
+		}
+
 		if objectType == "self" || objectType == "this" {
 			return &InvalidTypeError{ObjectType: objectType, Cause: ErrReservedKeywords}
 		}
 
 		for relation := range td.GetRelations() {
+			if relation == "" {
+				return fmt.Errorf("type '%s' defines a relation with an empty string for a name", objectType)
+			}
+
 			if relation == "self" || relation == "this" {
 				return &InvalidRelationError{ObjectType: objectType, Relation: relation, Cause: ErrReservedKeywords}
 			}
 		}
 	}
@@ -760,58 +994,52 @@ func (t *TypeSystem) isUsersetRewriteValid(objectType, relation string, rewrite
 	}
 
 	return nil
 }
 
-func (t *TypeSystem) validateRelationTypeRestrictions() error {
-	for objectType := range t.typeDefinitions {
-		relations, err := t.GetRelations(objectType)
-		if err != nil {
-			return err
-		}
+// validateTypeRestrictions validates the type restrictions of a given relation using the following rules:
+//  1. An assignable relation must have one or more type restrictions.
+//  2. A non-assignable relation must not have any type restrictions.
+//  3. For each type restriction referenced for an assignable relation, each of the referenced types and relations
+//     must be defined in the model.
+//  4. If the provided relation is a tupleset relation, then the type restriction must be on a direct object.
+func (t *TypeSystem) validateTypeRestrictions(objectType string, relationName string) error {
 
-		for name, relation := range relations {
-			relatedTypes := relation.GetTypeInfo().GetDirectlyRelatedUserTypes()
-			assignable := t.IsDirectlyAssignable(relation)
+	relation, err := t.GetRelation(objectType, relationName)
+	if err != nil {
+		return err
+	}
 
-			if assignable && len(relatedTypes) == 0 {
-				return AssignableRelationError(objectType, name)
-			}
+	relatedTypes := relation.GetTypeInfo().GetDirectlyRelatedUserTypes()
+	assignable := t.IsDirectlyAssignable(relation)
 
-			if assignable && len(relatedTypes) == 1 {
-				relatedObjectType := relatedTypes[0].GetType()
-				relatedRelation := relatedTypes[0].GetRelation()
-				if objectType == relatedObjectType && name == relatedRelation {
-					return &InvalidRelationError{ObjectType: objectType, Relation: name, Cause: ErrCycle}
-				}
-			}
+	if assignable && len(relatedTypes) == 0 {
+		return AssignableRelationError(objectType, relationName)
+	}
 
-			if !assignable && len(relatedTypes) != 0 {
-				return NonAssignableRelationError(objectType, name)
-			}
+	if !assignable && len(relatedTypes) != 0 {
+		return NonAssignableRelationError(objectType, relationName)
+	}
 
-			for _, related := range relatedTypes {
-				relatedObjectType := related.GetType()
-				relatedRelation := related.GetRelation()
+	for _, related := range relatedTypes {
+		relatedObjectType := related.GetType()
+		relatedRelation := related.GetRelation()
 
-				if _, err := t.GetRelations(relatedObjectType); err != nil {
-					return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation)
-				}
+		if _, err := t.GetRelations(relatedObjectType); err != nil {
+			return InvalidRelationTypeError(objectType, relationName, relatedObjectType, relatedRelation)
+		}
 
-				if related.GetRelationOrWildcard() != nil {
-					// The type of the relation cannot contain a userset or wildcard if the relation is a tupleset relation.
-					if ok, _ := t.IsTuplesetRelation(objectType, name); ok {
-						return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation)
-					}
+		if related.GetRelationOrWildcard() != nil {
+			// The type of the relation cannot contain a userset or wildcard if the relation is a tupleset relation.
+			if ok, _ := t.IsTuplesetRelation(objectType, relationName); ok {
+				return InvalidRelationTypeError(objectType, relationName, relatedObjectType, relatedRelation)
+			}
 
-					if relatedRelation != "" {
-						if _, err := t.GetRelation(relatedObjectType, relatedRelation); err != nil {
-							return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation)
-						}
-					}
+			if relatedRelation != "" {
+				if _, err := t.GetRelation(relatedObjectType, relatedRelation); err != nil {
+					return InvalidRelationTypeError(objectType, relationName, relatedObjectType, relatedRelation)
 				}
-
 			}
 		}
 	}
 
 	return nil
diff --git a/pkg/typesystem/typesystem_test.go b/pkg/typesystem/typesystem_test.go
index 83fe032..179c759 100644
--- a/pkg/typesystem/typesystem_test.go
+++ b/pkg/typesystem/typesystem_test.go
@@ -1,15 +1,152 @@
 package typesystem
 
 import (
+	"context"
 	"testing"
 
 	parser "github.com/craigpastro/openfga-dsl-parser/v2"
 	"github.com/stretchr/testify/require"
 	openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1"
 )
 
+func TestNewAndValidate(t *testing.T) {
+
+	tests := []struct {
+		name          string
+		model         string
+		expectedError error
+	}{
+		{
+			name: "direct_relationship_with_entrypoint",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define viewer: [user] as self
+			`,
+		},
+		{
+			name: "computed_relationship_with_entrypoint",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define editor: [user] as self
+			    define viewer as editor
+			`,
+		},
+		{
+			name: "no_entrypoint_1",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define admin: [user] as self
+			    define action1 as admin and action2 and action3
+			    define action2 as admin and action1 and action3
+			    define action3 as admin and action1 and action2
+			`,
+			expectedError: ErrNoEntrypoints,
+		},
+		{
+			name: "no_entrypoint_2",
+			model: `
+			type user
+
+			type document
+			  relations
+				define admin: [user] as self
+				define action1 as admin but not action2
+				define action2 as admin but not action3
+				define action3 as admin but not action1
+			`,
+			expectedError: ErrNoEntrypoints,
+		},
+		{
+			name: "no_entrypoint_3a",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define viewer: [document#viewer] as self and editor
+			    define editor: [user] as self
+			`,
+			expectedError: ErrNoEntrypoints,
+		},
+		{
+			name: "no_entrypoint_3b",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define viewer: [document#viewer] as self but not editor
+			    define editor: [user] as self
+			`,
+			expectedError: ErrNoEntrypoints,
+		},
+		{
+			name: "no_entrypoint_4",
+			model: `
+			type user
+
+			type folder
+			  relations
+			    define parent: [document] as self
+			    define viewer as editor from parent
+
+			type document
+			  relations
+			    define parent: [folder] as self
+				define editor as viewer
+			    define viewer as editor from parent
+			`,
+			expectedError: ErrNoEntrypoints,
+		},
+		{
+			name: "self_referencing_type_restriction_with_entrypoint_1",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define restricted: [user] as self
+			    define editor: [user] as self
+			    define viewer: [document#viewer] as self or editor
+			    define can_view as viewer but not restricted
+			    define can_view_actual as can_view
+			`,
+		},
+		{
+			name: "self_referencing_type_restriction_with_entrypoint_2",
+			model: `
+			type user
+
+			type document
+			  relations
+			    define editor: [user] as self
+			    define viewer: [document#viewer] as self or editor
+			`,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			_, err := NewAndValidate(context.Background(), &openfgapb.AuthorizationModel{
+				SchemaVersion:   SchemaVersion1_1,
+				TypeDefinitions: parser.MustParse(test.model),
+			})
+			require.ErrorIs(t, err, test.expectedError)
+		})
+	}
+}
+
 func TestSuccessfulRewriteValidations(t *testing.T) {
 	var tests = []struct {
 		name  string
 		model *openfgapb.AuthorizationModel
 	}{
@@ -34,15 +171,55 @@ func TestSuccessfulRewriteValidations(t *testing.T) {
 						Relations: map[string]*openfgapb.Userset{},
 					},
 				},
 			},
 		},
+		{
+			name: "self_referencing_type_restriction_with_entrypoint",
+			model: &openfgapb.AuthorizationModel{
+				TypeDefinitions: parser.MustParse(`
+				type user
+
+				type document
+				  relations
+				    define editor: [user] as self
+				    define viewer: [document#viewer] as self or editor
+				`),
+				SchemaVersion: SchemaVersion1_1,
+			},
+		},
+		{
+			name: "intersection_may_contain_repeated_relations",
+			model: &openfgapb.AuthorizationModel{
+				TypeDefinitions: parser.MustParse(`
+				type user
+				type document
+				  relations
+					define editor: [user] as self
+					define viewer as editor and editor
+				`),
+				SchemaVersion: SchemaVersion1_1,
+			},
+		},
+		{
+			name: "exclusion_may_contain_repeated_relations",
+			model: &openfgapb.AuthorizationModel{
+				TypeDefinitions: parser.MustParse(`
+				type user
+				type document
+				  relations
+					define editor: [user] as self
+					define viewer as editor but not editor
+				`),
+				SchemaVersion: SchemaVersion1_1,
+			},
+		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := NewAndValidate(test.model)
+			_, err := NewAndValidate(context.Background(), test.model)
 			require.NoError(t, err)
 		})
 	}
 }
 
@@ -469,17 +646,17 @@ func TestInvalidRewriteValidations(t *testing.T) {
 				  relations
 				    define parent: [folder] as self
 					define viewer as viewer from parent
 				`),
 			},
-			err: ErrCycle,
+			err: ErrNoEntrypoints,
 		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := NewAndValidate(test.model)
+			_, err := NewAndValidate(context.Background(), test.model)
 			require.ErrorIs(t, err, test.err)
 		})
 	}
 }
 func TestSuccessfulRelationTypeRestrictionsValidations(t *testing.T) {
@@ -576,11 +753,11 @@ func TestSuccessfulRelationTypeRestrictionsValidations(t *testing.T) {
 		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := NewAndValidate(test.model)
+			_, err := NewAndValidate(context.Background(), test.model)
 			require.NoError(t, err)
 		})
 	}
 }
 
@@ -1050,11 +1227,11 @@ func TestInvalidRelationTypeRestrictionsValidations(t *testing.T) {
 		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := NewAndValidate(test.model)
+			_, err := NewAndValidate(context.Background(), test.model)
 			require.EqualError(t, err, test.err.Error())
 		})
 	}
 }
 

Vulnerable code: 					_, err := typesystem.NewAndValidate(model)
	typesys := typesystem.New(model)
	_, err := typesystem.NewAndValidate(model)
	typesys := typesystem.New(model)
	typesys := typesystem.New(model)
	typesys := typesystem.New(model)
		ResolveNodeLimit: 25,
		ResolveNodeLimit: 25,
		ResolveNodeLimit:      25,
		ResolveNodeLimit:      25,
		ResolveNodeLimit:      25,
			ResolveNodeLimit:      25,
				test.resolveNodeLimit = defaultResolveNodeLimit
							},
						},
					},
				},
			},
			tuples: []*openfgapb.TupleKey{
				tuple.NewTupleKey("document:1", "parent", "document:2#editor"),
			},
			request: &openfgapb.ExpandRequest{
				TupleKey: tuple.NewTupleKey("document:1", "parent", ""),
			},
			expected: &openfgapb.ExpandResponse{
				Tree: &openfgapb.UsersetTree{
					Root: &openfgapb.UsersetTree_Node{
						Name: "document:1#parent",
						Value: &openfgapb.UsersetTree_Node_Leaf{
							Leaf: &openfgapb.UsersetTree_Leaf{
								Value: &openfgapb.UsersetTree_Leaf_Users{
									Users: &openfgapb.UsersetTree_Users{
										Users: []string{"document:2#editor"},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "1.1_TupleToUserset_involving_wildcard_is_skipped",
			model: &openfgapb.AuthorizationModel{
				Id:            ulid.Make().String(),
				SchemaVersion: typesystem.SchemaVersion1_1,
				TypeDefinitions: []*openfgapb.TypeDefinition{
					{
						Type: "user",
					},
					{
						Type: "document",
						Relations: map[string]*openfgapb.Userset{
							"parent": typesystem.This(),
							"viewer": typesystem.Union(
								typesystem.This(),
								typesystem.TupleToUserset("parent", "viewer"),
							),
						},
						Metadata: &openfgapb.Metadata{
							Relations: map[string]*openfgapb.RelationMetadata{
								"parent": {
										typesystem.WildcardRelationReference("user"),
			tuples: []*openfgapb.TupleKey{
				tuple.NewTupleKey("document:1", "parent", "user:*"),
				tuple.NewTupleKey("document:X", "viewer", "user:jon"),
			},
			request: &openfgapb.ExpandRequest{
				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
			},
			expected: &openfgapb.ExpandResponse{
				Tree: &openfgapb.UsersetTree{
					Root: &openfgapb.UsersetTree_Node{
						Name: "document:1#viewer",
						Value: &openfgapb.UsersetTree_Node_Union{
							Union: &openfgapb.UsersetTree_Nodes{
								Nodes: []*openfgapb.UsersetTree_Node{
									{
										Name: "document:1#viewer",
										Value: &openfgapb.UsersetTree_Node_Leaf{
											Leaf: &openfgapb.UsersetTree_Leaf{
												Value: &openfgapb.UsersetTree_Leaf_Users{},
											},
										},
									},
									{
										Name: "document:1#viewer",
										Value: &openfgapb.UsersetTree_Node_Leaf{
											Leaf: &openfgapb.UsersetTree_Leaf{
												Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
													TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
														Tupleset: "document:1#parent",
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "1.1_Tuple_involving_userset_skipped_if_it_is_referenced_in_a_TTU_rewrite",
			model: &openfgapb.AuthorizationModel{
				Id:            ulid.Make().String(),
				SchemaVersion: typesystem.SchemaVersion1_1,
				TypeDefinitions: []*openfgapb.TypeDefinition{
					{
						Type: "folder",
						Relations: map[string]*openfgapb.Userset{
							"viewer": typesystem.This(),
						},
					},
					{
						Type: "document",
						Relations: map[string]*openfgapb.Userset{
							"parent": typesystem.This(),
							"editor": typesystem.This(),
							"viewer": typesystem.TupleToUserset("parent", "viewer"),
						},
						Metadata: &openfgapb.Metadata{
							Relations: map[string]*openfgapb.RelationMetadata{
								"parent": {
									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
										typesystem.DirectRelationReference("document", "editor"),
									},
								},
							},
						},
					},
				},
			},
				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
						Name: "document:1#viewer",
								Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
									TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
										Tupleset: "document:1#parent",
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "1.1_Tuple_involving_userset_skipped_if_same_ComputedUserset_involved_in_TTU_rewrite",
			model: &openfgapb.AuthorizationModel{
				Id:            ulid.Make().String(),
				SchemaVersion: typesystem.SchemaVersion1_1,
				TypeDefinitions: []*openfgapb.TypeDefinition{
					{
						Type: "user",
					},
					{
						Type: "document",
						Relations: map[string]*openfgapb.Userset{
							"parent": typesystem.This(),
							"viewer": typesystem.Union(
								typesystem.This(),
								typesystem.TupleToUserset("parent", "viewer"),
							),
						},
						Metadata: &openfgapb.Metadata{
							Relations: map[string]*openfgapb.RelationMetadata{
								"viewer": {
									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
										{Type: "user"},
									},
								},
								"parent": {
									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
										typesystem.DirectRelationReference("document", "viewer"),
									},
								},
							},
						},
					},
				},
			},
			tuples: []*openfgapb.TupleKey{
				tuple.NewTupleKey("document:1", "parent", "document:2#viewer"),
				tuple.NewTupleKey("document:2", "viewer", "user:jon"),
			},
			request: &openfgapb.ExpandRequest{
				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
			},
			expected: &openfgapb.ExpandResponse{
				Tree: &openfgapb.UsersetTree{
					Root: &openfgapb.UsersetTree_Node{
						Name: "document:1#viewer",
						Value: &openfgapb.UsersetTree_Node_Union{
							Union: &openfgapb.UsersetTree_Nodes{
								Nodes: []*openfgapb.UsersetTree_Node{
									{
										Name: "document:1#viewer",
										Value: &openfgapb.UsersetTree_Node_Leaf{
											Leaf: &openfgapb.UsersetTree_Leaf{
												Value: &openfgapb.UsersetTree_Leaf_Users{},
											},
										},
									},
									{
										Name: "document:1#viewer",
										Value: &openfgapb.UsersetTree_Node_Leaf{
											Leaf: &openfgapb.UsersetTree_Leaf{
												Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
													TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
														Tupleset: "document:1#parent",
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "1.1_Tupleset_relation_involving_rewrite_skipped",
			model: &openfgapb.AuthorizationModel{
				Id:            ulid.Make().String(),
				SchemaVersion: typesystem.SchemaVersion1_1,
				TypeDefinitions: []*openfgapb.TypeDefinition{
					{
						Type: "user",
					},
					{
						Type: "document",
						Relations: map[string]*openfgapb.Userset{
							"parent": typesystem.ComputedUserset("editor"),
							"editor": typesystem.This(),
							"viewer": typesystem.Union(
								typesystem.This(), typesystem.TupleToUserset("parent", "viewer"),
							),
						},
						Metadata: &openfgapb.Metadata{
							Relations: map[string]*openfgapb.RelationMetadata{
								"editor": {
									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
										{Type: "document"},
									},
								},
								"viewer": {
									DirectlyRelatedUserTypes: []*openfgapb.RelationReference{
										{Type: "user"},
									},
								},
							},
						},
					},
				},
			},
			tuples: []*openfgapb.TupleKey{
				tuple.NewTupleKey("document:1", "editor", "document:2"),
				tuple.NewTupleKey("document:2", "viewer", "user:jon"),
			},
			request: &openfgapb.ExpandRequest{
				TupleKey: tuple.NewTupleKey("document:1", "viewer", ""),
			},
			expected: &openfgapb.ExpandResponse{
				Tree: &openfgapb.UsersetTree{
					Root: &openfgapb.UsersetTree_Node{
						Name: "document:1#viewer",
						Value: &openfgapb.UsersetTree_Node_Union{
							Union: &openfgapb.UsersetTree_Nodes{
								Nodes: []*openfgapb.UsersetTree_Node{
									{
										Name: "document:1#viewer",
										Value: &openfgapb.UsersetTree_Node_Leaf{
											Leaf: &openfgapb.UsersetTree_Leaf{
												Value: &openfgapb.UsersetTree_Leaf_Users{},
											},
										},
									},
									{
										Name: "document:1#viewer",
										Value: &openfgapb.UsersetTree_Node_Leaf{
											Leaf: &openfgapb.UsersetTree_Leaf{
												Value: &openfgapb.UsersetTree_Leaf_TupleToUserset{
													TupleToUserset: &openfgapb.UsersetTree_TupleToUserset{
														Tupleset: "document:1#parent",
													},
												},
											},
										},
				ResolveNodeLimit:      defaultResolveNodeLimit,
		ResolveNodeLimit: defaultResolveNodeLimit,
		ResolveNodeLimit: defaultResolveNodeLimit,
	defaultResolveNodeLimit = 25

		tds[td.GetType()] = td
		relations[td.GetType()] = tdRelations
func NewAndValidate(model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
	// Validate the userset rewrites
	for _, td := range t.typeDefinitions {
		for relation, rewrite := range td.GetRelations() {
			err := t.isUsersetRewriteValid(td.GetType(), relation, rewrite)
	if schemaVersion == SchemaVersion1_1 {
		if err := t.validateRelationTypeRestrictions(); err != nil {
			return nil, err
	return t, nil
func (t *TypeSystem) validateRelationTypeRestrictions() error {
	for objectType := range t.typeDefinitions {
		relations, err := t.GetRelations(objectType)
		if err != nil {
			return err
		}
		for name, relation := range relations {
			relatedTypes := relation.GetTypeInfo().GetDirectlyRelatedUserTypes()
			assignable := t.IsDirectlyAssignable(relation)
			if assignable && len(relatedTypes) == 0 {
				return AssignableRelationError(objectType, name)
			}
			if assignable && len(relatedTypes) == 1 {
				relatedObjectType := relatedTypes[0].GetType()
				relatedRelation := relatedTypes[0].GetRelation()
				if objectType == relatedObjectType && name == relatedRelation {
					return &InvalidRelationError{ObjectType: objectType, Relation: name, Cause: ErrCycle}
				}
			}
			if !assignable && len(relatedTypes) != 0 {
				return NonAssignableRelationError(objectType, name)
			}
			for _, related := range relatedTypes {
				relatedObjectType := related.GetType()
				relatedRelation := related.GetRelation()
				if _, err := t.GetRelations(relatedObjectType); err != nil {
					return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation)
				}
				if related.GetRelationOrWildcard() != nil {
					// The type of the relation cannot contain a userset or wildcard if the relation is a tupleset relation.
					if ok, _ := t.IsTuplesetRelation(objectType, name); ok {
						return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation)
					}
					if relatedRelation != "" {
						if _, err := t.GetRelation(relatedObjectType, relatedRelation); err != nil {
							return InvalidRelationTypeError(objectType, name, relatedObjectType, relatedRelation)
						}
					}

			_, err := NewAndValidate(test.model)
			err: ErrCycle,
			_, err := NewAndValidate(test.model)
			_, err := NewAndValidate(test.model)
			_, err := NewAndValidate(test.model)
Fixed code: 					_, err := typesystem.NewAndValidate(context.Background(), model)
	typesys, err := typesystem.NewAndValidate(ctx, model)
	if err != nil {
		return nil, serverErrors.ValidationError(typesystem.ErrInvalidModel)
	}
	_, err := typesystem.NewAndValidate(ctx, model)
	typesys, err := typesystem.NewAndValidate(ctx, model)
	if err != nil {
		return nil, serverErrors.ValidationError(typesystem.ErrInvalidModel)
	}
	typesys, err := typesystem.NewAndValidate(ctx, model)
	if err != nil {
		return serverErrors.ValidationError(typesystem.ErrInvalidModel)
	}
	typesys, err := typesystem.NewAndValidate(ctx, model)
	if err != nil {
		return nil, serverErrors.ValidationError(typesystem.ErrInvalidModel)
	}
		ResolveNodeLimit: test.DefaultResolveNodeLimit,
func TestOperationsWithInvalidModel(t *testing.T) {
	ctx := context.Background()
	storeID := ulid.Make().String()
	modelID := ulid.Make().String()

	// The model is invalid
	typedefs := parser.MustParse(`
	type user

	type repo
	  relations
        define admin: [user] as self
	    define r1: [user] as self and r2 and r3
	    define r2: [user] as self and r1 and r3
	    define r3: [user] as self and r1 and r2
	`)

	tk := tuple.NewTupleKey("repo:openfga", "r1", "user:anne")
	mockController := gomock.NewController(t)
	defer mockController.Finish()

	mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController)

	mockDatastore.EXPECT().
		ReadAuthorizationModel(gomock.Any(), storeID, modelID).
		AnyTimes().
		Return(&openfgapb.AuthorizationModel{
			SchemaVersion:   typesystem.SchemaVersion1_1,
			TypeDefinitions: typedefs,
		}, nil)

	// the model is error and err should return

	s := New(&Dependencies{
		Datastore: mockDatastore,
		Logger:    logger.NewNoopLogger(),
		Transport: gateway.NewNoopTransport(),
	}, &Config{
		ResolveNodeLimit: test.DefaultResolveNodeLimit,
	})

	_, err := s.Check(ctx, &openfgapb.CheckRequest{
		StoreId:              storeID,
		TupleKey:             tk,
		AuthorizationModelId: modelID,
	})
	require.Error(t, err)
	e, ok := status.FromError(err)
	require.True(t, ok)
	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())

	_, err = s.ListObjects(ctx, &openfgapb.ListObjectsRequest{
		StoreId:              storeID,
		AuthorizationModelId: modelID,
		Type:                 "repo",
		Relation:             "r1",
		User:                 "user:anne",
	})
	require.Error(t, err)
	e, ok = status.FromError(err)
	require.True(t, ok)
	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())

	err = s.StreamedListObjects(&openfgapb.StreamedListObjectsRequest{
		StoreId:              storeID,
		AuthorizationModelId: modelID,
		Type:                 "repo",
		Relation:             "r1",
		User:                 "user:anne",
	}, NewMockStreamServer())
	require.Error(t, err)
	e, ok = status.FromError(err)
	require.True(t, ok)
	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())

	_, err = s.Expand(ctx, &openfgapb.ExpandRequest{
		StoreId:              storeID,
		AuthorizationModelId: modelID,
		TupleKey:             tk,
	})
	require.Error(t, err)
	e, ok = status.FromError(err)
	require.True(t, ok)
	require.Equal(t, codes.Code(openfgapb.ErrorCode_validation_error), e.Code())

}

		ResolveNodeLimit: test.DefaultResolveNodeLimit,
		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
			ResolveNodeLimit:      test.DefaultResolveNodeLimit,
				test.resolveNodeLimit = DefaultResolveNodeLimit
					{
						Type: "user",
					},
					{
						Type: "user",
					},
								"editor": {
				TupleKey: tuple.NewTupleKey("document:1", "parent", ""),
						Name: "document:1#parent",
								Value: &openfgapb.UsersetTree_Leaf_Users{
									Users: &openfgapb.UsersetTree_Users{
										Users: []string{"document:2#editor"},
				ResolveNodeLimit:      DefaultResolveNodeLimit,
		ResolveNodeLimit: DefaultResolveNodeLimit,
		ResolveNodeLimit: DefaultResolveNodeLimit,
	DefaultResolveNodeLimit = 25
	parser "github.com/craigpastro/openfga-dsl-parser/v2"
		{
			name: "self_referencing_type_restriction_with_entrypoint",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define editor: [user] as self
				    define viewer: [document#viewer] as self or editor
				`),
				SchemaVersion: typesystem.SchemaVersion1_1,
			},
		},
		{
			name: "self_referencing_type_restriction_without_entrypoint_1",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user
				type document
				  relations
				    define viewer: [document#viewer] as self
				`),
				SchemaVersion: typesystem.SchemaVersion1_1,
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "viewer",
				Cause:      typesystem.ErrCycle,
			}),
		},
		{
			name: "self_referencing_type_restriction_without_entrypoint_2",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user
				type document
				  relations
				    define editor: [user] as self
				    define viewer: [document#viewer] as self and editor
				`),
				SchemaVersion: typesystem.SchemaVersion1_1,
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "viewer",
				Cause:      typesystem.ErrNoEntrypoints,
			}),
		},
		{
			name: "self_referencing_type_restriction_without_entrypoint_3",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user
				type document
				  relations
				    define restricted: [user] as self
				    define viewer: [document#viewer] as self but not restricted
				`),
				SchemaVersion: typesystem.SchemaVersion1_1,
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "viewer",
				Cause:      typesystem.ErrCycle,
			}),
		},
		{
			name: "rewritten_relation_in_intersection_unresolvable",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define admin: [user] as self
				    define action1 as admin and action2 and action3
				    define action2 as admin and action1 and action3
				    define action3 as admin and action1 and action2
				`),
				SchemaVersion: typesystem.SchemaVersion1_1,
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "action1",
				Cause:      typesystem.ErrNoEntrypoints,
			}),
		},
		{
			name: "direct_relationship_with_entrypoint",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define viewer: [user] as self
				`),
			},
		},
		{
			name: "computed_relationship_with_entrypoint",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define editor: [user] as self
				    define viewer as editor
				`),
			},
		},

		{
			name: "rewritten_relation_in_exclusion_unresolvable",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define admin: [user] as self
				    define action1 as admin but not action2
				    define action2 as admin but not action3
				    define action3 as admin but not action1
				`),
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "action1",
				Cause:      typesystem.ErrNoEntrypoints,
			}),
		},
		{
			name: "no_entrypoint_3a",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define viewer: [document#viewer] as self and editor
				    define editor: [user] as self
				`),
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "viewer",
				Cause:      typesystem.ErrNoEntrypoints,
			}),
		},

		{
			name: "no_entrypoint_3b",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define viewer: [document#viewer] as self but not editor
				    define editor: [user] as self
				`),
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "viewer",
				Cause:      typesystem.ErrNoEntrypoints,
			}),
		},
		{
			name: "no_entrypoint_4",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type folder
				  relations
				    define parent: [document] as self
				    define viewer as editor from parent

				type document
				  relations
				    define parent: [folder] as self
				    define editor as viewer
				    define viewer as editor from parent
				`),
			},
			err: serverErrors.InvalidAuthorizationModelInput(&typesystem.InvalidRelationError{
				ObjectType: "document",
				Relation:   "editor",
				Cause:      typesystem.ErrNoEntrypoints,
			}),
		},
		{
			name: "self_referencing_type_restriction_with_entrypoint_1",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define restricted: [user] as self
				    define editor: [user] as self
				    define viewer: [document#viewer] as self or editor
				    define can_view as viewer but not restricted
				    define can_view_actual as can_view
				`),
			},
		},
		{
			name: "self_referencing_type_restriction_with_entrypoint_2",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define editor: [user] as self
				    define viewer: [document#viewer] as self or editor
				`),
			},
		},
		{
			name: "relation_with_union_of_ttu_rewrites",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: parser.MustParse(`
				type user
				type org
				  relations
				    define admin: [user] as self
				    define member: [user] as self
				type group
				  relations
				    define member: [user] as self
				type feature
				  relations
				    define accessible as admin from subscriber_org or member from subscriber_group
				    define subscriber_group: [group] as self
				    define subscriber_org: [org] as self
				`),
			},
		},
		{
			name: "type_name_is_empty_string",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: []*openfgapb.TypeDefinition{
					{
						Type: "",
					},
				},
			},
			err: serverErrors.InvalidAuthorizationModelInput(
				fmt.Errorf("the type name of a type definition cannot be an empty string"),
			),
		},
		{
			name: "relation_name_is_empty_string",
			request: &openfgapb.WriteAuthorizationModelRequest{
				StoreId: storeID,
				TypeDefinitions: []*openfgapb.TypeDefinition{
					{
						Type: "user",
						Relations: map[string]*openfgapb.Userset{
							"": typesystem.This(),
						},
					},
					{
						Type: "other",
					},
				},
			},
			err: serverErrors.InvalidAuthorizationModelInput(
				fmt.Errorf("type 'user' defines a relation with an empty string for a name"),
			),
		},
	"sort"
	"go.opentelemetry.io/otel"
	"golang.org/x/exp/maps"
var tracer = otel.Tracer("openfga/pkg/typesystem")

	ErrNoEntrypoints         = errors.New("no entrypoints defined")
		typeName := td.GetType()

		tds[typeName] = td
		relations[typeName] = tdRelations
// hasEntrypoints recursively walks the rewrite definition for the given relation to determine if there is at least
// one path in the rewrite rule that could relate to at least one concrete object type. If there is no such path that
// could lead to at least one relationship with some object type, then false is returned along with an error indicating
// no entrypoints were found. If at least one relationship with a specific object type is found while walking the rewrite,
// then true is returned along with a nil error.
func hasEntrypoints(
	typedefs map[string]map[string]*openfgapb.Relation,
	typeName, relationName string,
	rewrite *openfgapb.Userset,
	visitedRelations map[string]map[string]struct{},
) (bool, error) {

	v := maps.Clone(visitedRelations)

	if val, ok := v[typeName]; ok {
		val[relationName] = struct{}{}
	} else {
		v[typeName] = map[string]struct{}{
			relationName: {},
		}
	}

	relation, ok := typedefs[typeName][relationName]
	if !ok {
		return false, fmt.Errorf("undefined type definition for '%s#%s'", typeName, relationName)
	}

	switch rw := rewrite.Userset.(type) {
	case *openfgapb.Userset_This:
		for _, assignableType := range relation.GetTypeInfo().GetDirectlyRelatedUserTypes() {
			if assignableType.GetRelationOrWildcard() == nil || assignableType.GetWildcard() != nil {
				return true, nil
			}

			assignableTypeName := assignableType.GetType()
			assignableRelationName := assignableType.GetRelation()

			assignableRelation, ok := typedefs[assignableTypeName][assignableRelationName]
			if !ok {
				return false, fmt.Errorf("undefined type definition for '%s#%s'", assignableTypeName, assignableRelationName)
			}

			if _, ok := v[assignableTypeName][assignableRelationName]; ok {
				continue
			}

			hasEntrypoint, err := hasEntrypoints(typedefs, assignableTypeName, assignableRelationName, assignableRelation.GetRewrite(), v)
			if err != nil {
				return false, err
			}

			if hasEntrypoint {
				return true, nil
			}
		}

		return false, nil
	case *openfgapb.Userset_ComputedUserset:

		computedRelationName := rw.ComputedUserset.GetRelation()
		computedRelation, ok := typedefs[typeName][computedRelationName]
		if !ok {
			return false, fmt.Errorf("undefined type definition for '%s#%s'", typeName, computedRelationName)
		}

		if _, ok := v[typeName][computedRelationName]; ok {
			return false, nil
		}

		hasEntrypoint, err := hasEntrypoints(typedefs, typeName, computedRelationName, computedRelation.GetRewrite(), v)
		if err != nil {
			return false, err
		}

		return hasEntrypoint, nil
	case *openfgapb.Userset_TupleToUserset:
		tuplesetRelationName := rw.TupleToUserset.GetTupleset().GetRelation()
		computedRelationName := rw.TupleToUserset.ComputedUserset.GetRelation()

		tuplesetRelation, ok := typedefs[typeName][tuplesetRelationName]
		if !ok {
			return false, fmt.Errorf("undefined type definition for '%s#%s'", typeName, tuplesetRelationName)
		}

		for _, assignableType := range tuplesetRelation.GetTypeInfo().GetDirectlyRelatedUserTypes() {
			assignableTypeName := assignableType.GetType()

			if assignableRelation, ok := typedefs[assignableTypeName][computedRelationName]; ok {
				if _, ok := v[assignableTypeName][computedRelationName]; ok {
					continue
				}

				hasEntrypoint, err := hasEntrypoints(typedefs, assignableTypeName, computedRelationName, assignableRelation.GetRewrite(), v)
				if err != nil {
					return false, err
				}

				if hasEntrypoint {
					return true, nil
				}
			}
		}

		return false, nil

	case *openfgapb.Userset_Union:

		for _, child := range rw.Union.Child {

			hasEntrypoints, err := hasEntrypoints(typedefs, typeName, relationName, child, maps.Clone(visitedRelations))
			if err != nil {
				return false, err
			}

			if hasEntrypoints {
				return true, nil
			}
		}

		return false, nil
	case *openfgapb.Userset_Intersection:

		for _, child := range rw.Intersection.Child {

			// all of the children must have an entrypoint
			hasEntrypoints, err := hasEntrypoints(typedefs, typeName, relationName, child, maps.Clone(visitedRelations))
			if err != nil {
				return false, err
			}

			if !hasEntrypoints {
				return false, nil
			}
		}

		return true, nil
	case *openfgapb.Userset_Difference:

		v := maps.Clone(visitedRelations)

		hasEntrypoint, err := hasEntrypoints(typedefs, typeName, relationName, rw.Difference.GetBase(), v)
		if err != nil {
			return false, err
		}

		if !hasEntrypoint {
			return false, nil
		}

		hasEntrypoint, err = hasEntrypoints(typedefs, typeName, relationName, rw.Difference.GetSubtract(), v)
		if err != nil {
			return false, err
		}

		if !hasEntrypoint {
			return false, nil
		}

		return true, nil
	}

	return false, nil
}

func NewAndValidate(ctx context.Context, model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
	_, span := tracer.Start(ctx, "typesystem.NewAndValidate")
	defer span.End()

	typedefsMap := t.typeDefinitions

	typeNames := make([]string, 0, len(typedefsMap))
	for typeName := range typedefsMap {
		typeNames = append(typeNames, typeName)
	}

	// range over the type definitions in sorted order to produce a deterministic outcome
	sort.Strings(typeNames)

	for _, typeName := range typeNames {
		typedef := typedefsMap[typeName]

		relationMap := typedef.GetRelations()
		relationNames := make([]string, 0, len(relationMap))
		for relationName := range relationMap {
			relationNames = append(relationNames, relationName)
		}

		// range over the relations in sorted order to produce a deterministic outcome
		sort.Strings(relationNames)

		for _, relationName := range relationNames {

			err := t.validateRelation(typeName, relationName, relationMap)
	return t, nil
}

// validateRelation applies all of the validation rules to a relation definition in a model. A relation
// must meet all of the rewrite validation, type restriction valdiation, and entrypoint validation criteria
// for it to be valid. Otherrwise an error is returned.
func (t *TypeSystem) validateRelation(typeName, relationName string, relationMap map[string]*openfgapb.Userset) error {

	rewrite := relationMap[relationName]

	err := t.isUsersetRewriteValid(typeName, relationName, rewrite)
	if err != nil {
		return err
	}

	err = t.validateTypeRestrictions(typeName, relationName)
	if err != nil {
		return err
	}

	visitedRelations := map[string]map[string]struct{}{}

	hasEntrypoints, err := hasEntrypoints(t.relations, typeName, relationName, rewrite, visitedRelations)
	if err != nil {
		return err
	}

	if !hasEntrypoints {
		return &InvalidRelationError{
			ObjectType: typeName,
			Relation:   relationName,
			Cause:      fmt.Errorf("no entrypoints found for relation '%s#%s': %w", typeName, relationName, ErrNoEntrypoints),
	return nil

		if objectType == "" {
			return fmt.Errorf("the type name of a type definition cannot be an empty string")
		}

			if relation == "" {
				return fmt.Errorf("type '%s' defines a relation with an empty string for a name", objectType)
			}

// validateTypeRestrictions validates the type restrictions of a given relation using the following rules:
//  1. An assignable relation must have one or more type restrictions.
//  2. A non-assignable relation must not have any type restrictions.
//  3. For each type restriction referenced for an assignable relation, each of the referenced types and relations
//     must be defined in the model.
//  4. If the provided relation is a tupleset relation, then the type restriction must be on a direct object.
func (t *TypeSystem) validateTypeRestrictions(objectType string, relationName string) error {
	relation, err := t.GetRelation(objectType, relationName)
	if err != nil {
		return err
	}
	relatedTypes := relation.GetTypeInfo().GetDirectlyRelatedUserTypes()
	assignable := t.IsDirectlyAssignable(relation)
	if assignable && len(relatedTypes) == 0 {
		return AssignableRelationError(objectType, relationName)
	}
	if !assignable && len(relatedTypes) != 0 {
		return NonAssignableRelationError(objectType, relationName)
	}
	for _, related := range relatedTypes {
		relatedObjectType := related.GetType()
		relatedRelation := related.GetRelation()
		if _, err := t.GetRelations(relatedObjectType); err != nil {
			return InvalidRelationTypeError(objectType, relationName, relatedObjectType, relatedRelation)
		}
		if related.GetRelationOrWildcard() != nil {
			// The type of the relation cannot contain a userset or wildcard if the relation is a tupleset relation.
			if ok, _ := t.IsTuplesetRelation(objectType, relationName); ok {
				return InvalidRelationTypeError(objectType, relationName, relatedObjectType, relatedRelation)
			}
			if relatedRelation != "" {
				if _, err := t.GetRelation(relatedObjectType, relatedRelation); err != nil {
					return InvalidRelationTypeError(objectType, relationName, relatedObjectType, relatedRelation)
	"context"
func TestNewAndValidate(t *testing.T) {

	tests := []struct {
		name          string
		model         string
		expectedError error
	}{
		{
			name: "direct_relationship_with_entrypoint",
			model: `
			type user

			type document
			  relations
			    define viewer: [user] as self
			`,
		},
		{
			name: "computed_relationship_with_entrypoint",
			model: `
			type user

			type document
			  relations
			    define editor: [user] as self
			    define viewer as editor
			`,
		},
		{
			name: "no_entrypoint_1",
			model: `
			type user

			type document
			  relations
			    define admin: [user] as self
			    define action1 as admin and action2 and action3
			    define action2 as admin and action1 and action3
			    define action3 as admin and action1 and action2
			`,
			expectedError: ErrNoEntrypoints,
		},
		{
			name: "no_entrypoint_2",
			model: `
			type user

			type document
			  relations
				define admin: [user] as self
				define action1 as admin but not action2
				define action2 as admin but not action3
				define action3 as admin but not action1
			`,
			expectedError: ErrNoEntrypoints,
		},
		{
			name: "no_entrypoint_3a",
			model: `
			type user

			type document
			  relations
			    define viewer: [document#viewer] as self and editor
			    define editor: [user] as self
			`,
			expectedError: ErrNoEntrypoints,
		},
		{
			name: "no_entrypoint_3b",
			model: `
			type user

			type document
			  relations
			    define viewer: [document#viewer] as self but not editor
			    define editor: [user] as self
			`,
			expectedError: ErrNoEntrypoints,
		},
		{
			name: "no_entrypoint_4",
			model: `
			type user

			type folder
			  relations
			    define parent: [document] as self
			    define viewer as editor from parent

			type document
			  relations
			    define parent: [folder] as self
				define editor as viewer
			    define viewer as editor from parent
			`,
			expectedError: ErrNoEntrypoints,
		},
		{
			name: "self_referencing_type_restriction_with_entrypoint_1",
			model: `
			type user

			type document
			  relations
			    define restricted: [user] as self
			    define editor: [user] as self
			    define viewer: [document#viewer] as self or editor
			    define can_view as viewer but not restricted
			    define can_view_actual as can_view
			`,
		},
		{
			name: "self_referencing_type_restriction_with_entrypoint_2",
			model: `
			type user

			type document
			  relations
			    define editor: [user] as self
			    define viewer: [document#viewer] as self or editor
			`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := NewAndValidate(context.Background(), &openfgapb.AuthorizationModel{
				SchemaVersion:   SchemaVersion1_1,
				TypeDefinitions: parser.MustParse(test.model),
			})
			require.ErrorIs(t, err, test.expectedError)
		})
	}
}

		{
			name: "self_referencing_type_restriction_with_entrypoint",
			model: &openfgapb.AuthorizationModel{
				TypeDefinitions: parser.MustParse(`
				type user

				type document
				  relations
				    define editor: [user] as self
				    define viewer: [document#viewer] as self or editor
				`),
				SchemaVersion: SchemaVersion1_1,
			},
		},
		{
			name: "intersection_may_contain_repeated_relations",
			model: &openfgapb.AuthorizationModel{
				TypeDefinitions: parser.MustParse(`
				type user
				type document
				  relations
					define editor: [user] as self
					define viewer as editor and editor
				`),
				SchemaVersion: SchemaVersion1_1,
			},
		},
		{
			name: "exclusion_may_contain_repeated_relations",
			model: &openfgapb.AuthorizationModel{
				TypeDefinitions: parser.MustParse(`
				type user
				type document
				  relations
					define editor: [user] as self
					define viewer as editor but not editor
				`),
				SchemaVersion: SchemaVersion1_1,
			},
		},
			_, err := NewAndValidate(context.Background(), test.model)
			err: ErrNoEntrypoints,
			_, err := NewAndValidate(context.Background(), test.model)
			_, err := NewAndValidate(context.Background(), test.model)
			_, err := NewAndValidate(context.Background(), test.model)