-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ca86c23
commit 8dffd44
Showing
8 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package graph | ||
|
||
import ( | ||
"errors" | ||
|
||
"gonum.org/v1/gonum/graph/encoding" | ||
"gonum.org/v1/gonum/graph/encoding/dot" | ||
"gonum.org/v1/gonum/graph/multi" | ||
) | ||
|
||
var ErrBuildingGraph = errors.New("cannot build graph") | ||
|
||
type AuthorizationModelGraph struct { | ||
*multi.DirectedGraph | ||
} | ||
|
||
var _ dot.Attributers = (*AuthorizationModelGraph)(nil) | ||
|
||
func (g *AuthorizationModelGraph) DOTAttributers() (graph, node, edge encoding.Attributer) { | ||
return g, nil, nil | ||
} | ||
|
||
func (g *AuthorizationModelGraph) Attributes() []encoding.Attribute { | ||
// https://graphviz.org/docs/attrs/rankdir/ - bottom to top | ||
return []encoding.Attribute{{ | ||
Key: "rankdir", | ||
Value: "BT", | ||
}} | ||
} | ||
|
||
// GetDOT returns the DOT visualization. The output text is stable. | ||
// It should only be used for debugging. | ||
func (g *AuthorizationModelGraph) GetDOT() string { | ||
dotRepresentation, err := dot.MarshalMulti(g, "", "", "") | ||
if err != nil { | ||
return "" | ||
} | ||
|
||
return string(dotRepresentation) | ||
} | ||
|
||
// TODO add graph traversals, cycle detection, etc. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package graph | ||
|
||
import ( | ||
"cmp" | ||
"fmt" | ||
"slices" | ||
|
||
openfgav1 "github.com/openfga/api/proto/openfga/v1" | ||
"gonum.org/v1/gonum/graph" | ||
"gonum.org/v1/gonum/graph/multi" | ||
) | ||
|
||
type AuthorizationModelGraphBuilder struct { | ||
graph.DirectedMultigraphBuilder | ||
|
||
ids map[string]int64 // 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. | ||
// TODO expand when more use cases are added. | ||
func NewAuthorizationModelGraph(model *openfgav1.AuthorizationModel) (*AuthorizationModelGraph, error) { | ||
res, err := parseModel(model) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &AuthorizationModelGraph{res}, nil | ||
} | ||
|
||
func parseModel(model *openfgav1.AuthorizationModel) (*multi.DirectedGraph, error) { | ||
graphBuilder := &AuthorizationModelGraphBuilder{ | ||
multi.NewDirectedGraph(), map[string]int64{}, | ||
} | ||
|
||
// sort types by name to guarantee stable output | ||
sortedTypeDefs := make([]*openfgav1.TypeDefinition, len(model.GetTypeDefinitions())) | ||
copy(sortedTypeDefs, model.GetTypeDefinitions()) | ||
|
||
slices.SortFunc(sortedTypeDefs, func(a, b *openfgav1.TypeDefinition) int { | ||
return cmp.Compare(a.GetType(), b.GetType()) | ||
}) | ||
|
||
for _, typeDef := range sortedTypeDefs { | ||
graphBuilder.GetOrAddNode(typeDef.GetType(), typeDef.GetType(), SpecificType) | ||
|
||
// sort relations by name to guarantee stable output | ||
sortedRelations := make([]string, 0, len(typeDef.GetRelations())) | ||
for relationName := range typeDef.GetRelations() { | ||
sortedRelations = append(sortedRelations, relationName) | ||
} | ||
|
||
slices.Sort(sortedRelations) | ||
|
||
for _, relation := range sortedRelations { | ||
uniqueLabel := fmt.Sprintf("%s#%s", typeDef.GetType(), relation) | ||
relationNode := graphBuilder.GetOrAddNode(uniqueLabel, uniqueLabel, SpecificTypeAndRelation) | ||
|
||
rewrite := typeDef.GetRelations()[relation] | ||
switch rewrite.GetUserset().(type) { | ||
case *openfgav1.Userset_This: | ||
directlyRelated := make([]*openfgav1.RelationReference, 0) | ||
if metadata, ok := typeDef.GetMetadata().GetRelations()[relation]; ok { | ||
directlyRelated = metadata.GetDirectlyRelatedUserTypes() | ||
} | ||
|
||
for _, directlyRelatedDef := range directlyRelated { | ||
assignableType := directlyRelatedDef.GetType() | ||
|
||
newNode := graphBuilder.GetOrAddNode(assignableType, assignableType, SpecificType) | ||
graphBuilder.AddEdge(newNode, relationNode, DirectEdge) | ||
} | ||
} | ||
} | ||
} | ||
|
||
multigraph, ok := graphBuilder.DirectedMultigraphBuilder.(*multi.DirectedGraph) | ||
if ok { | ||
return multigraph, nil | ||
} | ||
|
||
return nil, fmt.Errorf("%w: could not cast to directed graph", ErrBuildingGraph) | ||
} | ||
|
||
func (g *AuthorizationModelGraphBuilder) GetOrAddNode(uniqueLabel, label string, nodeType NodeType) *AuthorizationModelNode { | ||
if existingNode := g.GetNodeFor(uniqueLabel); existingNode != nil { | ||
return existingNode | ||
} | ||
|
||
node := g.NewNode() | ||
nodeid := node.ID() | ||
newNode := &AuthorizationModelNode{ | ||
Node: node, | ||
label: label, | ||
nodeType: nodeType, | ||
uniqueLabel: uniqueLabel, | ||
} | ||
g.AddNode(newNode) | ||
g.ids[uniqueLabel] = nodeid | ||
|
||
return newNode | ||
} | ||
|
||
func (g *AuthorizationModelGraphBuilder) GetNodeFor(uniqueLabel string) *AuthorizationModelNode { | ||
id, ok := g.ids[uniqueLabel] | ||
if !ok { | ||
return nil | ||
} | ||
|
||
authModelNode, ok := g.Node(id).(*AuthorizationModelNode) | ||
if !ok { | ||
return nil | ||
} | ||
|
||
return authModelNode | ||
} | ||
|
||
func (g *AuthorizationModelGraphBuilder) AddEdge(from, to graph.Node, edgeType EdgeType) *AuthorizationModelEdge { | ||
l := g.NewLine(from, to) | ||
newLine := &AuthorizationModelEdge{Line: l, edgeType: edgeType} | ||
g.SetLine(newLine) | ||
|
||
return newLine | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package graph | ||
|
||
import ( | ||
"sort" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/stretchr/testify/require" | ||
|
||
language "github.com/openfga/language/pkg/go/transformer" | ||
) | ||
|
||
// TestGetDOTRepresentation also tests that the graph is built correctly. | ||
func TestGetDOTRepresentation(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := map[string]struct { | ||
model string | ||
expectedOutput string | ||
}{ | ||
`direct_assignment`: { | ||
model: ` | ||
model | ||
schema 1.1 | ||
type folder | ||
relations | ||
define viewer: [user] | ||
type user`, | ||
expectedOutput: `digraph { | ||
graph [ | ||
rankdir=BT | ||
]; | ||
// Node definitions. | ||
0 [label=folder]; | ||
1 [label="folder#viewer"]; | ||
2 [label=user]; | ||
// Edge definitions. | ||
2 -> 1 [label=direct]; | ||
}`, | ||
}, | ||
} | ||
|
||
for name, test := range testCases { | ||
test := test | ||
|
||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
model := language.MustTransformDSLToProto(test.model) | ||
graph, err := NewAuthorizationModelGraph(model) | ||
require.NoError(t, err) | ||
|
||
actualDOT := graph.GetDOT() | ||
actualSorted := getSorted(actualDOT) | ||
expectedSorted := getSorted(test.expectedOutput) | ||
|
||
diff := cmp.Diff(expectedSorted, actualSorted) | ||
|
||
require.Empty(t, diff, "expected %s\ngot %s", test.expectedOutput, actualDOT) | ||
}) | ||
} | ||
} | ||
|
||
// getSorted assumes the input has multiple lines and returns the sorted version of it. | ||
func getSorted(input string) string { | ||
lines := strings.FieldsFunc(input, func(r rune) bool { | ||
return r == '\n' | ||
}) | ||
|
||
sort.Strings(lines) | ||
|
||
return strings.Join(lines, "\n") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package graph | ||
|
||
import ( | ||
"gonum.org/v1/gonum/graph" | ||
"gonum.org/v1/gonum/graph/encoding" | ||
) | ||
|
||
type EdgeType int64 | ||
|
||
const ( | ||
DirectEdge EdgeType = 0 // e.g. `group` | ||
) | ||
|
||
type AuthorizationModelEdge struct { | ||
graph.Line | ||
|
||
// custom attributes | ||
edgeType EdgeType | ||
} | ||
|
||
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", | ||
}) | ||
} | ||
|
||
return attrs | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package graph | ||
|
||
import ( | ||
"gonum.org/v1/gonum/graph" | ||
"gonum.org/v1/gonum/graph/encoding" | ||
) | ||
|
||
type NodeType int64 | ||
|
||
const ( | ||
SpecificType NodeType = 0 // e.g. `group` | ||
SpecificTypeAndRelation NodeType = 1 // e.g. `group#viewer` | ||
) | ||
|
||
type AuthorizationModelNode struct { | ||
graph.Node | ||
|
||
// custom attributes | ||
label string // e.g. `union`, for DOT | ||
nodeType NodeType | ||
uniqueLabel string // e.g. `union[a,b]` | ||
} | ||
|
||
var _ encoding.Attributer = (*AuthorizationModelNode)(nil) | ||
|
||
func (n *AuthorizationModelNode) Attributes() []encoding.Attribute { | ||
var attrs []encoding.Attribute | ||
|
||
attrs = append(attrs, encoding.Attribute{ | ||
Key: "label", | ||
Value: n.label, | ||
}) | ||
|
||
return attrs | ||
} |