diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f6181ff7..ef6c0574 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ * @openfga/dx @openfga/contractors-ides README.md @openfga/product @openfga/community @openfga/dx +pkg/go/graph/* @openfga/backend diff --git a/pkg/go/graph/graph.go b/pkg/go/graph/graph.go index 52dfebc8..257b791f 100644 --- a/pkg/go/graph/graph.go +++ b/pkg/go/graph/graph.go @@ -11,18 +11,48 @@ import ( "gonum.org/v1/gonum/graph/topo" ) -var ErrBuildingGraph = errors.New("cannot build graph") +var ( + ErrBuildingGraph = errors.New("cannot build graph") + ErrQueryingGraph = errors.New("cannot query graph") +) type DrawingDirection bool const ( + // DrawingDirectionListObjects is when terminal types have outgoing edges and no incoming edges. DrawingDirectionListObjects DrawingDirection = true - DrawingDirectionCheck DrawingDirection = false + // DrawingDirectionCheck is when terminal types have incoming edges and no outgoing edges. + DrawingDirectionCheck DrawingDirection = false ) type AuthorizationModelGraph struct { *multi.DirectedGraph drawingDirection DrawingDirection + ids NodeLabelsToIDs +} + +func (g *AuthorizationModelGraph) GetDrawingDirection() DrawingDirection { + return g.drawingDirection +} + +// GetNodeByLabel provides O(1) access to a node. +func (g *AuthorizationModelGraph) GetNodeByLabel(label string) (*AuthorizationModelNode, error) { + id, ok := g.ids[label] + if !ok { + return nil, fmt.Errorf("%w: node with label %s not found", ErrQueryingGraph, label) + } + + node := g.Node(id) + if node == nil { + return nil, fmt.Errorf("%w: node with id %d not found", ErrQueryingGraph, id) + } + + casted, ok := node.(*AuthorizationModelNode) + if !ok { + return nil, fmt.Errorf("%w: could not cast to AuthorizationModelNode", ErrQueryingGraph) + } + + return casted, nil } // Reversed returns a full copy of the graph, but with the direction of the arrows flipped. @@ -57,9 +87,15 @@ func (g *AuthorizationModelGraph) Reversed() (*AuthorizationModelGraph, error) { } } + // Make a brand new copy of the map. + copyIDs := make(NodeLabelsToIDs, len(g.ids)) + for k, v := range g.ids { + copyIDs[k] = v + } + multigraph, ok := graphBuilder.DirectedMultigraphBuilder.(*multi.DirectedGraph) if ok { - return &AuthorizationModelGraph{multigraph, !g.drawingDirection}, nil + return &AuthorizationModelGraph{multigraph, !g.drawingDirection, copyIDs}, nil } return nil, fmt.Errorf("%w: could not cast to directed graph", ErrBuildingGraph) diff --git a/pkg/go/graph/graph_builder.go b/pkg/go/graph/graph_builder.go index 26baf347..b30b8e97 100644 --- a/pkg/go/graph/graph_builder.go +++ b/pkg/go/graph/graph_builder.go @@ -11,29 +11,28 @@ import ( "gonum.org/v1/gonum/graph/multi" ) +type NodeLabelsToIDs map[string]int64 + type AuthorizationModelGraphBuilder struct { graph.DirectedMultigraphBuilder - ids map[string]int64 // nodes: unique labels to ids. Used to find nodes by label. + ids NodeLabelsToIDs // nodes: unique labels to ids. Used to find nodes by label. } // NewAuthorizationModelGraph builds an authorization model in graph form. // For example, types such as `group`, usersets such as `group#member` and wildcards `group:*` are encoded as nodes. -// -// The edges are defined by the assignments, e.g. -// `define viewer: [group]` defines an edge from group to document#viewer. -// Conditions are not encoded in the graph, -// and the two edges in an exclusion are not distinguished. +// By default, the graph is drawn from bottom to top (i.e. terminal types have outgoing edges and no incoming edges). +// Conditions are not encoded in the graph. func NewAuthorizationModelGraph(model *openfgav1.AuthorizationModel) (*AuthorizationModelGraph, error) { - res, err := parseModel(model) + res, ids, err := parseModel(model) if err != nil { return nil, err } - return &AuthorizationModelGraph{res, DrawingDirectionListObjects}, nil + return &AuthorizationModelGraph{res, DrawingDirectionListObjects, ids}, nil } -func parseModel(model *openfgav1.AuthorizationModel) (*multi.DirectedGraph, error) { +func parseModel(model *openfgav1.AuthorizationModel) (*multi.DirectedGraph, NodeLabelsToIDs, error) { graphBuilder := &AuthorizationModelGraphBuilder{ multi.NewDirectedGraph(), map[string]int64{}, } @@ -67,10 +66,10 @@ func parseModel(model *openfgav1.AuthorizationModel) (*multi.DirectedGraph, erro multigraph, ok := graphBuilder.DirectedMultigraphBuilder.(*multi.DirectedGraph) if ok { - return multigraph, nil + return multigraph, graphBuilder.ids, nil } - return nil, fmt.Errorf("%w: could not cast to directed graph", ErrBuildingGraph) + return nil, nil, fmt.Errorf("%w: could not cast to directed graph", ErrBuildingGraph) } func checkRewrite(graphBuilder *AuthorizationModelGraphBuilder, parentNode *AuthorizationModelNode, model *openfgav1.AuthorizationModel, rewrite *openfgav1.Userset, typeDef *openfgav1.TypeDefinition, relation string) { @@ -248,10 +247,7 @@ func (g *AuthorizationModelGraphBuilder) HasEdge(from, to graph.Node, edgeType E } iter := g.Lines(from.ID(), to.ID()) - for { - if !iter.Next() { - return false - } + for iter.Next() { l := iter.Line() edge, ok := l.(*AuthorizationModelEdge) if !ok { @@ -261,6 +257,8 @@ func (g *AuthorizationModelGraphBuilder) HasEdge(from, to graph.Node, edgeType E return true } } + + return false } func typeAndRelationExists(model *openfgav1.AuthorizationModel, typeName, relation string) bool { diff --git a/pkg/go/graph/graph_edge.go b/pkg/go/graph/graph_edge.go index ba00feda..10495995 100644 --- a/pkg/go/graph/graph_edge.go +++ b/pkg/go/graph/graph_edge.go @@ -26,34 +26,41 @@ type AuthorizationModelEdge struct { var _ encoding.Attributer = (*AuthorizationModelEdge)(nil) -func (n *AuthorizationModelEdge) Attributes() []encoding.Attribute { - var attrs []encoding.Attribute - - if n.edgeType == DirectEdge { - attrs = append(attrs, encoding.Attribute{ - Key: "label", - Value: "direct", - }) - } - - if n.edgeType == ComputedEdge { - attrs = append(attrs, encoding.Attribute{ - Key: "style", - Value: "dashed", - }) - } +func (n *AuthorizationModelEdge) EdgeType() EdgeType { + return n.edgeType +} - if n.edgeType == TTUEdge { +func (n *AuthorizationModelEdge) Attributes() []encoding.Attribute { + switch n.edgeType { + case DirectEdge: + return []encoding.Attribute{ + { + Key: "label", + Value: "direct", + }, + } + case ComputedEdge: + return []encoding.Attribute{ + { + Key: "style", + Value: "dashed", + }, + } + case TTUEdge: headLabelAttrValue := n.conditionedOn if headLabelAttrValue == "" { headLabelAttrValue = "missing" } - attrs = append(attrs, encoding.Attribute{ - Key: "headlabel", - Value: headLabelAttrValue, - }) + return []encoding.Attribute{ + { + Key: "headlabel", + Value: headLabelAttrValue, + }, + } + case RewriteEdge: + return []encoding.Attribute{} + default: + return []encoding.Attribute{} } - - return attrs } diff --git a/pkg/go/graph/graph_test.go b/pkg/go/graph/graph_test.go index 2d863c65..7cdf35d5 100644 --- a/pkg/go/graph/graph_test.go +++ b/pkg/go/graph/graph_test.go @@ -1,6 +1,7 @@ package graph import ( + "strconv" "testing" "github.com/google/go-cmp/cmp" @@ -71,10 +72,8 @@ rankdir=TB model := language.MustTransformDSLToProto(testCase.model) graph, err := NewAuthorizationModelGraph(model) require.NoError(t, err) - require.Equal(t, DrawingDirectionListObjects, graph.drawingDirection) reversedGraph, err := graph.Reversed() require.NoError(t, err) - require.Equal(t, DrawingDirectionCheck, reversedGraph.drawingDirection) actualDOT := reversedGraph.GetDOT() actualSorted := getSorted(actualDOT) expectedSorted := getSorted(testCase.expectedOutput) @@ -85,3 +84,78 @@ rankdir=TB }) } } + +func TestGetDrawingDirection(t *testing.T) { + t.Parallel() + model := language.MustTransformDSLToProto(` + model + schema 1.1 + type user + type company + relations + define member: [user]`) + graph, err := NewAuthorizationModelGraph(model) + require.NoError(t, err) + require.Equal(t, DrawingDirectionListObjects, graph.GetDrawingDirection()) + reversedGraph, err := graph.Reversed() + require.NoError(t, err) + require.Equal(t, DrawingDirectionCheck, reversedGraph.GetDrawingDirection()) +} + +func TestGetNodeByLabel(t *testing.T) { + t.Parallel() + model := language.MustTransformDSLToProto(` + model + schema 1.1 + type user + type company + relations + define member: [user with cond, user:* with cond] + define owner: [user] + define approved_member: member or owner + type group + relations + define approved_member: [user] + type license + relations + define active_member: approved_member from owner + define owner: [company, group]`) + graph, err := NewAuthorizationModelGraph(model) + require.NoError(t, err) + + testCases := []struct { + label string + expectedFound bool + }{ + // found + {"user", true}, + {"user:*", true}, + {"company", true}, + {"company#member", true}, + {"company#owner", true}, + {"company#approved_member", true}, + {"group", true}, + {"group#approved_member", true}, + {"license", true}, + {"license#active_member", true}, + {"license#owner", true}, + // not found + {"unknown", false}, + {"unknown#unknown", false}, + {"user with cond", false}, + {"user:* with cond", false}, + } + for i, testCase := range testCases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + node, err := graph.GetNodeByLabel(testCase.label) + if testCase.expectedFound { + require.NoError(t, err) + require.NotNil(t, node) + } else { + require.ErrorIs(t, err, ErrQueryingGraph) + require.Nil(t, node) + } + }) + } +} diff --git a/pkg/js/package-lock.json b/pkg/js/package-lock.json index abcc054d..30006231 100644 --- a/pkg/js/package-lock.json +++ b/pkg/js/package-lock.json @@ -1541,16 +1541,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", - "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", + "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/type-utils": "8.5.0", - "@typescript-eslint/utils": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/type-utils": "8.6.0", + "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1574,15 +1574,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", - "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", + "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4" }, "engines": { @@ -1602,13 +1602,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", - "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", + "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0" + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1619,13 +1619,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", - "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", + "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.5.0", - "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/typescript-estree": "8.6.0", + "@typescript-eslint/utils": "8.6.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1643,9 +1643,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", - "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", + "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1656,13 +1656,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", - "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", + "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/visitor-keys": "8.5.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/visitor-keys": "8.6.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1684,15 +1684,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", + "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.5.0", - "@typescript-eslint/types": "8.5.0", - "@typescript-eslint/typescript-estree": "8.5.0" + "@typescript-eslint/scope-manager": "8.6.0", + "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/typescript-estree": "8.6.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1706,12 +1706,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", - "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", + "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": {