Skip to content

Commit 02ef40c

Browse files
authored
Feat/grouping userset ttus (#493)
* add optimizer.go * added new logical units for grouping ttus and usersets * delete the optimizer.go * added review comments * review comments * addressed review comments * added test case for logical ttu * rebase from main and tests
1 parent efa08b0 commit 02ef40c

File tree

7 files changed

+490
-249
lines changed

7 files changed

+490
-249
lines changed

pkg/go/graph/graph_edge.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,6 @@ import (
55
"gonum.org/v1/gonum/graph/encoding"
66
)
77

8-
type EdgeType int64
9-
10-
const (
11-
DirectEdge EdgeType = 0
12-
RewriteEdge EdgeType = 1
13-
TTUEdge EdgeType = 2
14-
ComputedEdge EdgeType = 3
15-
// When an edge does not have cond in the model, it will have a condition with value none.
16-
// This is required to differentiate when an edge need to support condition and no condition
17-
// like define rel1: [user, user with condX], in this case the edge will have [none, condX]
18-
// or an edge needs to support only condition like define rel1: [user with condX], the edge will have [condX]
19-
// in the case the edge does not have any condition like define rel1: [user], the edge will have [none].
20-
NoCond string = "none"
21-
)
22-
238
type AuthorizationModelEdge struct {
249
graph.Line
2510

pkg/go/graph/graph_node.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,6 @@ import (
55
"gonum.org/v1/gonum/graph/encoding"
66
)
77

8-
type NodeType int64
9-
10-
const (
11-
SpecificType NodeType = 0 // e.g. `group`
12-
SpecificTypeAndRelation NodeType = 1 // e.g. `group#viewer`
13-
OperatorNode NodeType = 2 // e.g. union
14-
SpecificTypeWildcard NodeType = 3 // e.g. `group:*`
15-
16-
UnionOperator = "union"
17-
IntersectionOperator = "intersection"
18-
ExclusionOperator = "exclusion"
19-
)
20-
218
type AuthorizationModelNode struct {
229
graph.Node
2310

pkg/go/graph/weighted_graph.go

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ func (wg *WeightedAuthorizationModelGraph) AssignWeights() error {
129129
tupleCycleDependencies := make(map[string][]*WeightedAuthorizationModelEdge)
130130

131131
for nodeID, node := range wg.nodes {
132-
if node.GetNodeType() == OperatorNode {
133-
// Inititally defer weight assignment of operator nodes to later in `fixDependantNodesWeight()`.
132+
if wg.isLogicalOperator(node) {
133+
// Inititally defer weight assignment of operator nodes and the logical nodes to later in `fixDependantNodesWeight()`.
134134
// This enables more deterministic behavior of intermediate functions.
135135
continue
136136
}
@@ -150,6 +150,24 @@ func (wg *WeightedAuthorizationModelGraph) AssignWeights() error {
150150
return nil
151151
}
152152

153+
func (wg *WeightedAuthorizationModelGraph) isLogicalOperator(node *WeightedAuthorizationModelNode) bool {
154+
// a logical ttu is when a ttu has more than one edges due to having multiple terminal types as usersets,
155+
// and should always be treated as a union among all those ttu edges as they belong to the same logical ttu
156+
// a logical userset is when we have multiple asignations to terminal types or usersets, we need to treat that
157+
// as a union for all the direct edges
158+
nodeType := node.GetNodeType()
159+
return nodeType == OperatorNode || nodeType == LogicalTTUGrouping || nodeType == LogicalDirectGrouping
160+
}
161+
162+
func (wg *WeightedAuthorizationModelGraph) isLogicalUnionOperator(node *WeightedAuthorizationModelNode) bool {
163+
// a logical ttu is when a ttu has more than one edges due to having multiple terminal types as usersets,
164+
// and should always be treated as a union among all those ttu edges as they belong to the same logical ttu
165+
// a logical userset is when we have multiple asignations to terminal types or usersets, we need to treat that
166+
// as a union for all the direct edges
167+
nodeType := node.GetNodeType()
168+
return (nodeType == OperatorNode && node.GetLabel() == UnionOperator) || nodeType == LogicalTTUGrouping || nodeType == LogicalDirectGrouping
169+
}
170+
153171
func (wg *WeightedAuthorizationModelGraph) calculateEdgeWildcards(edge *WeightedAuthorizationModelEdge) {
154172
// if wildcards already exist, we don't need to calculate them
155173
if len(edge.wildcards) > 0 {
@@ -437,8 +455,9 @@ func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightFromTheEdges(nodeI
437455
return tupleCycles, nil
438456
}
439457

440-
// there is a cycle but the node is not responsible for the cycle and it is not an operator node
441-
if node.nodeType != OperatorNode {
458+
// there is a cycle but the node is not responsible for the cycle and it is not a logical operator node
459+
// a logical operator node is an operator node, a logical ttu and a logical userset node
460+
if !wg.isLogicalOperator(node) {
442461
// even when there is a cycle, if the relation is not recursive then we calculate the weight using the max strategy
443462
err = wg.calculateNodeWeightWithMaxStrategy(nodeID)
444463
if err != nil {
@@ -447,8 +466,9 @@ func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightFromTheEdges(nodeI
447466
return tupleCycles, nil
448467
}
449468

450-
// if the node is an union operator and there is a cycle
451-
if node.nodeType == OperatorNode && node.label == UnionOperator {
469+
// if the node is a logical union operator,
470+
// a logical union operator is a union node, a logical userset or a logical ttu.
471+
if wg.isLogicalUnionOperator(node) {
452472
// if the node is the reference node of the cycle, recalculate the weight, solve the depencies and remove the node from the tuple cycle
453473
if wg.isNodeTupleCycleReference(nodeID, tupleCycles) {
454474
err := wg.calculateNodeWeightAndFixDependencies(nodeID, tupleCycleDependencies)
@@ -497,30 +517,35 @@ func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightWithMaxStrategy(no
497517
// calculateNodeWeightWithMixedStrategy is a mixed weight strategy used for exclusion node (A but not B).
498518
// For all A edges, we take all the types for all the edges and get the max value
499519
// if more than one edge have the same type in their weights.
500-
// For all
501520
func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightWithMixedStrategy(nodeID string) error {
502521
node := wg.nodes[nodeID]
503-
weights := make(map[string]int)
504522
edges := wg.edges[nodeID]
505523

506524
if len(edges) == 0 && node.nodeType != SpecificType && node.nodeType != SpecificTypeWildcard {
507525
return fmt.Errorf("%w: %s node does not have any terminal type to reach to", ErrInvalidModel, node.uniqueLabel)
508526
}
509527

510-
for idx, edge := range edges {
511-
for key, value := range edge.weights {
512-
if _, ok := weights[key]; !ok {
513-
if idx != len(edges)-1 {
514-
// This is the A edge. We take the max weight of all key
515-
weights[key] = value
516-
} // otherwise, B edge requires weight to be present in A. Otherwise, we will ignore.
517-
} else {
518-
weights[key] = int(math.Max(float64(weights[key]), float64(value)))
519-
}
528+
if node.nodeType != OperatorNode || node.label != ExclusionOperator {
529+
return fmt.Errorf("%w: node %s cannot apply mixed strategy, only accepted exclusion nodes", ErrInvalidModel, nodeID)
530+
}
531+
532+
// with the logical ttu and userset, an exclusion operation only has two edges
533+
// the first edge is the one that defines the userset, the second edge is the one that excludes it
534+
if len(edges) != 2 {
535+
return fmt.Errorf("%w: invalid number of edges for exclusion node %s", ErrInvalidModel, nodeID)
536+
}
537+
nodeWeights := make(map[string]int, len(edges))
538+
for k, v := range edges[0].weights {
539+
nodeWeights[k] = v
540+
}
541+
542+
for key, value := range edges[1].weights {
543+
if w, ok := nodeWeights[key]; ok {
544+
nodeWeights[key] = int(math.Max(float64(w), float64(value)))
520545
}
521546
}
522547

523-
node.weights = weights
548+
node.weights = nodeWeights
524549
return nil
525550
}
526551

@@ -536,19 +561,9 @@ func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightWithEnforceTypeStr
536561
return fmt.Errorf("%w: %s node does not have any terminal type to reach to", ErrInvalidModel, node.uniqueLabel)
537562
}
538563

539-
// In intersection, directly-assignable types must be handled separately from rewritten types.
540-
// All types flatten to edges, but directly-assignable weights are OR'd together, and that result
541-
// is then AND'd with the combined weights of the rewritten edges for validity and weight assignment.
542-
directlyAssignableWeights := make(map[string]int, len(edges))
543564
rewriteWeights := make(map[string]int, len(edges))
544-
545565
for _, edge := range edges {
546-
if edge.GetEdgeType() == DirectEdge {
547-
for key, weight := range edge.weights {
548-
directlyAssignableWeights[key] = weight
549-
}
550-
continue
551-
}
566+
552567
if len(rewriteWeights) == 0 {
553568
for key, weight := range edge.weights {
554569
rewriteWeights[key] = weight
@@ -564,18 +579,7 @@ func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightWithEnforceTypeStr
564579
}
565580
}
566581

567-
if len(directlyAssignableWeights) > 0 {
568-
for key := range directlyAssignableWeights {
569-
if _, existsInBoth := rewriteWeights[key]; existsInBoth {
570-
if node.weights == nil {
571-
node.weights = make(map[string]int, int(math.Min(float64(len(rewriteWeights)), float64(len(directlyAssignableWeights)))))
572-
}
573-
node.weights[key] = int(math.Max(float64(rewriteWeights[key]), float64(directlyAssignableWeights[key])))
574-
}
575-
}
576-
} else {
577-
node.weights = rewriteWeights
578-
}
582+
node.weights = rewriteWeights
579583

580584
if len(node.weights) == 0 {
581585
return fmt.Errorf("%w: not all paths return the same type for the node %s", ErrInvalidModel, nodeID)
@@ -605,8 +609,8 @@ func (wg *WeightedAuthorizationModelGraph) calculateNodeWeightAndFixDependencies
605609
referenceNodeID := "R#" + nodeID
606610
edges := wg.edges[nodeID]
607611

608-
if (node.nodeType == OperatorNode && node.label != UnionOperator) || (node.nodeType != SpecificTypeAndRelation && node.nodeType != OperatorNode) {
609-
return fmt.Errorf("%w: invalid node, reference node is not a union operator or a relation: %s", ErrTupleCycle, nodeID)
612+
if node.nodeType != SpecificTypeAndRelation && !wg.isLogicalUnionOperator(node) {
613+
return fmt.Errorf("%w: invalid node, reference node is not a union operator or a relation or a logical userset or logical TTU: %s", ErrTupleCycle, nodeID)
610614
}
611615

612616
if len(edges) == 0 && node.nodeType != SpecificType && node.nodeType != SpecificTypeWildcard {

pkg/go/graph/weighted_graph_builder.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ func (wgb *WeightedAuthorizationModelGraphBuilder) parseTupleToUserset(wg *Weigh
122122
return fmt.Errorf("%w: Model cannot be parsed. No type and relation link exists for tupleset relation %s and computed relation %s", ErrInvalidModel, tuplesetRelation, computedRelation)
123123
}
124124

125+
typeTuplesetRelation := typeDef.GetType() + "#" + tuplesetRelation
126+
node := parentNode
127+
if parentNode.nodeType != SpecificTypeAndRelation && len(directlyRelated) > 1 {
128+
uniqueLabel := typeDef.GetType() + "#ttu:" + tuplesetRelation + "#" + computedRelation
129+
// add a logical ttu node for grouping of TTU that are part of the same tuplesetrelation and computed relation
130+
logicalNode := wg.GetOrAddNode(uniqueLabel, uniqueLabel, LogicalTTUGrouping)
131+
wg.AddEdge(parentNode.uniqueLabel, logicalNode.uniqueLabel, TTULogicalEdge, parentRelationName, typeTuplesetRelation, nil)
132+
node = logicalNode
133+
}
134+
125135
for _, relatedType := range directlyRelated {
126136
tuplesetType := relatedType.GetType()
127137

@@ -131,9 +141,8 @@ func (wgb *WeightedAuthorizationModelGraphBuilder) parseTupleToUserset(wg *Weigh
131141

132142
rewrittenNodeName := fmt.Sprintf("%s#%s", tuplesetType, computedRelation)
133143
nodeSource := wg.GetOrAddNode(rewrittenNodeName, rewrittenNodeName, SpecificTypeAndRelation)
134-
typeTuplesetRelation := typeDef.GetType() + "#" + tuplesetRelation
135144

136-
if wg.HasEdge(parentNode, nodeSource, TTUEdge, typeTuplesetRelation) {
145+
if wg.HasEdge(node, nodeSource, TTUEdge, typeTuplesetRelation) {
137146
// we don't need to do any condition update, only de-dup the edge. In case of TTU
138147
// the direct relation will have the conditions
139148
// for example, in the case of
@@ -147,7 +156,7 @@ func (wgb *WeightedAuthorizationModelGraphBuilder) parseTupleToUserset(wg *Weigh
147156
}
148157

149158
// new edge from "xxx#admin" to "yyy#viewer" tuplesetRelation on "yyy#parent"
150-
wg.UpsertEdge(parentNode, nodeSource, TTUEdge, parentRelationName, typeTuplesetRelation, relatedType.GetCondition())
159+
wg.UpsertEdge(node, nodeSource, TTUEdge, parentRelationName, typeTuplesetRelation, relatedType.GetCondition())
151160
}
152161
return nil
153162
}
@@ -171,6 +180,14 @@ func (wgb *WeightedAuthorizationModelGraphBuilder) parseThis(wg *WeightedAuthori
171180
if relationMetadata, ok := typeDef.GetMetadata().GetRelations()[relation]; ok {
172181
directlyRelated = relationMetadata.GetDirectlyRelatedUserTypes()
173182
}
183+
node := parentNode
184+
// add a logical userset node for grouping of direct usersets that are defined in the same relation
185+
if parentNode.nodeType != SpecificTypeAndRelation && len(directlyRelated) > 1 {
186+
uniqueLabel := typeDef.GetType() + "#direct:" + relation
187+
logicalNode := wg.GetOrAddNode(uniqueLabel, uniqueLabel, LogicalDirectGrouping)
188+
wg.AddEdge(parentNode.uniqueLabel, logicalNode.uniqueLabel, DirectLogicalEdge, parentRelationName, "", nil)
189+
node = logicalNode
190+
}
174191

175192
for _, directlyRelatedDef := range directlyRelated {
176193
switch {
@@ -190,7 +207,7 @@ func (wgb *WeightedAuthorizationModelGraphBuilder) parseThis(wg *WeightedAuthori
190207

191208
// de-dup types that are conditioned, e.g. if define viewer: [user, user with condX]
192209
// we only draw one edge from user to x#viewer, but with two conditions: none and condX
193-
err := wg.UpsertEdge(parentNode, curNode, DirectEdge, parentRelationName, "", directlyRelatedDef.GetCondition())
210+
err := wg.UpsertEdge(node, curNode, DirectEdge, parentRelationName, "", directlyRelatedDef.GetCondition())
194211
if err != nil {
195212
return err
196213
}

0 commit comments

Comments
 (0)