VYPR
Moderate severityNVD Advisory· Published Jun 26, 2023· Updated Dec 3, 2024

OpenFGA denial of service die to circular relationship

CVE-2023-35933

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/openfga/openfgaGo
< 1.1.11.1.1

Affected products

1

Patches

1
087ce392595f

Merge pull request from GHSA-hr9r-8phq-5x8j

https://github.com/openfga/openfgaJonathan WhitakerJun 26, 2023via ghsa
12 files changed · +881 352
  • cmd/validatemodels/validate_models.go+1 1 modified
    @@ -126,7 +126,7 @@ func ValidateAllAuthorizationModels(ctx context.Context, db storage.OpenFGADatas
     
     				// validate each model
     				for _, model := range models {
    -					_, err := typesystem.NewAndValidate(model)
    +					_, err := typesystem.NewAndValidate(context.Background(), model)
     
     					validationResult := validationResult{
     						StoreID:       store.Id,
    
  • pkg/server/commands/expand.go+4 1 modified
    @@ -51,7 +51,10 @@ func (q *ExpandQuery) Execute(ctx context.Context, req *openfgapb.ExpandRequest)
     		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)
    
  • pkg/server/commands/write_authzmodel.go+1 1 modified
    @@ -45,7 +45,7 @@ func (w *WriteAuthorizationModelCommand) Execute(ctx context.Context, req *openf
     		TypeDefinitions: req.GetTypeDefinitions(),
     	}
     
    -	_, err := typesystem.NewAndValidate(model)
    +	_, err := typesystem.NewAndValidate(ctx, model)
     	if err != nil {
     		return nil, serverErrors.InvalidAuthorizationModelInput(err)
     	}
    
  • pkg/server/server.go+12 3 modified
    @@ -105,7 +105,10 @@ 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)
     
    @@ -151,7 +154,10 @@ 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)
     
    @@ -237,7 +243,10 @@ func (s *Server) Check(ctx context.Context, req *openfgapb.CheckRequest) (*openf
     		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)
    
  • pkg/server/server_test.go+93 6 modified
    @@ -175,7 +175,7 @@ func TestCheckDoesNotThrowBecauseDirectTupleWasFound(t *testing.T) {
     		Logger:    logger.NewNoopLogger(),
     		Transport: gateway.NewNoopTransport(),
     	}, &Config{
    -		ResolveNodeLimit: 25,
    +		ResolveNodeLimit: test.DefaultResolveNodeLimit,
     	})
     
     	checkResponse, err := s.Check(ctx, &openfgapb.CheckRequest{
    @@ -187,6 +187,93 @@ func TestCheckDoesNotThrowBecauseDirectTupleWasFound(t *testing.T) {
     	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()
     
    @@ -245,7 +332,7 @@ func TestShortestPathToSolutionWins(t *testing.T) {
     		Logger:    logger.NewNoopLogger(),
     		Transport: gateway.NewNoopTransport(),
     	}, &Config{
    -		ResolveNodeLimit: 25,
    +		ResolveNodeLimit: test.DefaultResolveNodeLimit,
     	})
     
     	start := time.Now()
    @@ -380,7 +467,7 @@ func BenchmarkListObjectsNoRaceCondition(b *testing.B) {
     		Transport: transport,
     		Logger:    logger,
     	}, &Config{
    -		ResolveNodeLimit:      25,
    +		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
     		ListObjectsDeadline:   5 * time.Second,
     		ListObjectsMaxResults: 1000,
     	})
    @@ -440,7 +527,7 @@ func TestListObjects_Unoptimized_UnhappyPaths(t *testing.T) {
     		Transport: transport,
     		Logger:    logger,
     	}, &Config{
    -		ResolveNodeLimit:      25,
    +		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
     		ListObjectsDeadline:   5 * time.Second,
     		ListObjectsMaxResults: 1000,
     	})
    @@ -521,7 +608,7 @@ func TestListObjects_UnhappyPaths(t *testing.T) {
     		Transport: transport,
     		Logger:    logger,
     	}, &Config{
    -		ResolveNodeLimit:      25,
    +		ResolveNodeLimit:      test.DefaultResolveNodeLimit,
     		ListObjectsDeadline:   5 * time.Second,
     		ListObjectsMaxResults: 1000,
     	})
    @@ -587,7 +674,7 @@ func TestAuthorizationModelInvalidSchemaVersion(t *testing.T) {
     		transport: transport,
     		logger:    logger,
     		config: &Config{
    -			ResolveNodeLimit:      25,
    +			ResolveNodeLimit:      test.DefaultResolveNodeLimit,
     			ListObjectsDeadline:   5 * time.Second,
     			ListObjectsMaxResults: 1000,
     		},
    
  • pkg/server/test/connected_objects.go+1 1 modified
    @@ -908,7 +908,7 @@ func ConnectedObjectsTest(t *testing.T, ds storage.OpenFGADatastore) {
     			require.NoError(err)
     
     			if test.resolveNodeLimit == 0 {
    -				test.resolveNodeLimit = defaultResolveNodeLimit
    +				test.resolveNodeLimit = DefaultResolveNodeLimit
     			}
     
     			connectedObjectsCmd := commands.ConnectedObjectsCommand{
    
  • pkg/server/test/expand.go+12 279 modified
    @@ -90,6 +90,9 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
     				Id:            ulid.Make().String(),
     				SchemaVersion: typesystem.SchemaVersion1_1,
     				TypeDefinitions: []*openfgapb.TypeDefinition{
    +					{
    +						Type: "user",
    +					},
     					{
     						Type: "repo",
     						Relations: map[string]*openfgapb.Userset{
    @@ -811,6 +814,9 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
     				Id:            ulid.Make().String(),
     				SchemaVersion: typesystem.SchemaVersion1_1,
     				TypeDefinitions: []*openfgapb.TypeDefinition{
    +					{
    +						Type: "user",
    +					},
     					{
     						Type: "document",
     						Relations: map[string]*openfgapb.Userset{
    @@ -824,57 +830,8 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
     										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",
     										},
    @@ -885,245 +842,21 @@ func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
     					},
     				},
     			},
    -			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"},
     									},
     								},
     							},
    
  • pkg/server/test/list_objects.go+3 3 modified
    @@ -203,7 +203,7 @@ func TestListObjectsRespectsMaxResults(t *testing.T, ds storage.OpenFGADatastore
     				Logger:                logger.NewNoopLogger(),
     				ListObjectsDeadline:   listObjectsDeadline,
     				ListObjectsMaxResults: test.maxResults,
    -				ResolveNodeLimit:      defaultResolveNodeLimit,
    +				ResolveNodeLimit:      DefaultResolveNodeLimit,
     			}
     			typesys := typesystem.New(model)
     			ctx = typesystem.ContextWithTypesystem(ctx, typesys)
    @@ -313,7 +313,7 @@ func BenchmarkListObjectsWithReverseExpand(b *testing.B, ds storage.OpenFGADatas
     	listObjectsQuery := commands.ListObjectsQuery{
     		Datastore:        ds,
     		Logger:           logger.NewNoopLogger(),
    -		ResolveNodeLimit: defaultResolveNodeLimit,
    +		ResolveNodeLimit: DefaultResolveNodeLimit,
     	}
     
     	var r *openfgapb.ListObjectsResponse
    @@ -379,7 +379,7 @@ func BenchmarkListObjectsWithConcurrentChecks(b *testing.B, ds storage.OpenFGADa
     	listObjectsQuery := commands.ListObjectsQuery{
     		Datastore:        ds,
     		Logger:           logger.NewNoopLogger(),
    -		ResolveNodeLimit: defaultResolveNodeLimit,
    +		ResolveNodeLimit: DefaultResolveNodeLimit,
     	}
     
     	var r *openfgapb.ListObjectsResponse
    
  • pkg/server/test/server.go+1 2 modified
    @@ -7,7 +7,7 @@ import (
     )
     
     const (
    -	defaultResolveNodeLimit = 25
    +	DefaultResolveNodeLimit = 25
     )
     
     func RunAllTests(t *testing.T, ds storage.OpenFGADatastore) {
    @@ -19,7 +19,6 @@ 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) })
     
    
  • pkg/server/test/write_authzmodel.go+293 0 modified
    @@ -6,6 +6,7 @@ import (
     	"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"
    @@ -135,6 +136,298 @@ 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()
    
  • pkg/typesystem/typesystem.go+278 50 modified
    @@ -5,11 +5,16 @@ import (
     	"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 (
    @@ -28,6 +33,7 @@ var (
     	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 {
    @@ -150,7 +156,9 @@ func New(model *openfgapb.AuthorizationModel) *TypeSystem {
     	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() {
    @@ -166,7 +174,7 @@ func New(model *openfgapb.AuthorizationModel) *TypeSystem {
     
     			tdRelations[relation] = r
     		}
    -		relations[td.GetType()] = tdRelations
    +		relations[typeName] = tdRelations
     	}
     
     	return &TypeSystem{
    @@ -596,6 +604,170 @@ 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:
    @@ -610,7 +782,10 @@ func (t *TypeSystem) relationInvolvesExclusion(objectType, relation string, visi
     //     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()
     
    @@ -626,10 +801,31 @@ func NewAndValidate(model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
     		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
     			}
    @@ -644,13 +840,42 @@ func NewAndValidate(model *openfgapb.AuthorizationModel) (*TypeSystem, error) {
     		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 {
    @@ -670,11 +895,20 @@ func containsDuplicateType(model *openfgapb.AuthorizationModel) bool {
     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}
     			}
    @@ -762,54 +996,48 @@ 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)
     				}
    -
     			}
     		}
     	}
    
  • pkg/typesystem/typesystem_test.go+182 5 modified
    @@ -1,13 +1,150 @@
     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
    @@ -36,11 +173,51 @@ func TestSuccessfulRewriteValidations(t *testing.T) {
     				},
     			},
     		},
    +		{
    +			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)
     		})
     	}
    @@ -471,13 +648,13 @@ func TestInvalidRewriteValidations(t *testing.T) {
     					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)
     		})
     	}
    @@ -578,7 +755,7 @@ 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)
     		})
     	}
    @@ -1052,7 +1229,7 @@ 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())
     		})
     	}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.