Skip to content

add userset weights#505

Merged
yissellokta merged 3 commits intomainfrom
feat/userset_weight_function
Oct 20, 2025
Merged

add userset weights#505
yissellokta merged 3 commits intomainfrom
feat/userset_weight_function

Conversation

@yissellokta
Copy link
Copy Markdown
Contributor

@yissellokta yissellokta commented Oct 16, 2025

This PR provides a weight to traverse and prune branches that do not lead to the userset path, only valid when accessing the weight of a userset

Description

What problem is being solved?

How is it being solved?

What changes are made to solve it?

References

Review Checklist

  • I have clicked on "allow edits by maintainers".
  • I have added documentation for new/changed functionality in this PR or in a PR to openfga.dev [Provide a link to any relevant PRs in the references section above]
  • The correct base branch is being used, if not main
  • I have added tests to validate that the change in functionality is working as expected

Summary by CodeRabbit

  • New Features

    • Per-userset weighting for weighted graphs, enabling combined terminal-and-userset weight calculation, edge pruning, and multiple weight strategies for userset traversal.
    • Improved handling of recursive paths and cycles to propagate infinite and finite weights correctly.
  • Tests

    • Added comprehensive tests covering direct relationships, recursive paths, cycles, tuple dependencies, and expected weight outcomes.

@yissellokta yissellokta requested a review from a team as a code owner October 16, 2025 18:13
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 16, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Per-userset weighting added: nodes and edges gain usersetWeights maps; graph exposes GetWeight that delegates to userset-aware logic; new recursive, memoized userset weight computation handles pruning, cycles, and multiple calculation strategies.

Changes

Cohort / File(s) Summary
Core weighted graph implementation
pkg/go/graph/weighted_graph.go
Added userset-aware weight retrieval (GetWeight, getWeightForUserset); implemented recursive memoized userset weight computation (calculateUsersetWeights, cycle handling helpers: calculateUsersetNodeWeightWhenCycle, updateUsersetCycleWeight, finddUsersetWeightInCycle); added multiple weight strategies (Max, MaxWithEnforcement, HybridMax); added pruning (canPruneEdge) and setters (setUsersetWeightToNode, setUsersetWeightToEdge); updated node/edge initialization to allocate usersetWeights.
Node and edge struct extensions
pkg/go/graph/weighted_graph_node.go, pkg/go/graph/weighted_graph_edge.go
Added usersetWeights map[string]int field to WeightedAuthorizationModelNode and WeightedAuthorizationModelEdge.
Userset weight test coverage
pkg/go/graph/weighted_graph_userset_test.go
New tests exercising per-userset weight computation across direct, recursive, tuple-cycle, and dependent-recursive scenarios (multiple TestUsersetWeight* cases asserting Infinite, finite numeric, and absent weights).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Graph as WeightedAuthorizationModelGraph
    participant Cache as usersetWeights Cache
    participant Calc as calculateUsersetWeights

    Caller->>Graph: GetWeight(node, key)
    alt key contains "#" (userset)
        Graph->>Cache: lookup node.usersetWeights[userset]
        alt cached
            Cache-->>Graph: cached weight
        else not cached
            Graph->>Calc: calculateUsersetWeights(node, usersetNode, visited)
            Calc->>Calc: traverse edges, prune edges (canPruneEdge)
            Calc->>Calc: detect cycles -> cycle helpers
            Calc->>Cache: store computed weight on nodes/edges
            Calc-->>Graph: computed weight
        end
    else terminal/keyed weight
        Graph-->>Caller: return terminal weight if present
    end
    Graph-->>Caller: return (weight, ok)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jpadilla
  • justincoh

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "add userset weights" is directly and accurately related to the primary change in the changeset. The modifications across multiple files fundamentally introduce per-userset weighting support: new usersetWeights fields are added to node and edge structures, a comprehensive set of weight calculation methods are implemented for the WeightedAuthorizationModelGraph type, and substantial test coverage is added to validate this new functionality. The title is concise, clear, and specific enough that a teammate reviewing the commit history would immediately understand that this PR adds userset weight capabilities without any vague or misleading language.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/go/graph/weighted_graph.go (1)

802-1082: Consider concurrency safety and memory growth.

The new per-userset weight calculation methods use memoization maps (usersetWeights) without synchronization. Verify that the graph is only accessed from a single goroutine, or add synchronization if concurrent access is needed.

Additionally, the usersetWeights maps will grow with each unique userset queried. If the number of distinct usersets is large, consider adding a cleanup mechanism or size limits.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e9095d and 0a08501.

📒 Files selected for processing (4)
  • pkg/go/graph/weighted_graph.go (7 hunks)
  • pkg/go/graph/weighted_graph_edge.go (1 hunks)
  • pkg/go/graph/weighted_graph_node.go (1 hunks)
  • pkg/go/graph/weighted_graph_userset_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
pkg/go/graph/weighted_graph.go (2)
pkg/go/graph/weighted_graph_node.go (8)
  • WeightedAuthorizationModelNode (18-27)
  • SpecificTypeAndRelation (7-7)
  • LogicalDirectGrouping (10-10)
  • LogicalTTUGrouping (11-11)
  • OperatorNode (8-8)
  • UnionOperator (13-13)
  • IntersectionOperator (14-14)
  • ExclusionOperator (15-15)
pkg/go/graph/weighted_graph_edge.go (3)
  • WeightedAuthorizationModelEdge (58-76)
  • DirectEdge (11-11)
  • TTUEdge (21-21)
pkg/go/graph/weighted_graph_userset_test.go (3)
pkg/go/transformer/dsltojson.go (1)
  • MustTransformDSLToProto (553-560)
pkg/go/graph/weighted_graph_builder.go (1)
  • NewWeightedAuthorizationModelGraphBuilder (19-21)
pkg/go/graph/weighted_graph.go (1)
  • Infinite (11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (11)
pkg/go/graph/weighted_graph_edge.go (1)

74-75: LGTM!

The usersetWeights field addition follows the existing pattern of the weights field and is properly initialized in the graph methods.

pkg/go/graph/weighted_graph_node.go (1)

26-26: LGTM!

The usersetWeights field addition mirrors the existing weights field pattern and is properly initialized during node creation.

pkg/go/graph/weighted_graph_userset_test.go (1)

10-240: Comprehensive test coverage for userset weights.

The test suite effectively covers:

  • Direct paths with various operators (union, intersection, exclusion)
  • Recursive self-references
  • Tuple cycles between types
  • Dependencies on recursive definitions

The assertions verify both the presence of weights (for reachable paths) and absence (for pruned paths).

pkg/go/graph/weighted_graph.go (8)

57-57: LGTM! Proper initialization of usersetWeights maps.

The usersetWeights maps are correctly initialized in all node and edge creation paths, ensuring they're ready for use by the weight calculation logic.

Also applies to: 69-69, 79-79, 106-106


153-166: LGTM! Clean delegation in GetWeight method.

The method correctly distinguishes between userset keys (containing "#") and terminal type keys, delegating to the appropriate calculation path.


186-223: Effective early pruning optimization.

The method implements a multi-stage approach:

  1. Cache lookup for memoized results
  2. Node validation
  3. Early pruning based on terminal type weights (avoiding traversal when paths cannot exist)
  4. Full graph traversal when necessary

The early pruning logic at lines 206-214 correctly identifies unreachable paths by comparing terminal type weights.


802-895: Core traversal logic is sound with proper memoization and cycle handling.

The method correctly implements:

  • Memoization to avoid redundant calculations
  • Cycle detection to prevent infinite loops
  • Special handling for tuple cycles and recursive relations
  • Edge pruning to reduce unnecessary traversal
  • Weight propagation with appropriate increments for Direct/TTU edges
  • Strategy selection based on node type (union, intersection, exclusion)

897-994: Cycle handling logic correctly addresses recursive and tuple cycle scenarios.

The three-method approach effectively handles cycles:

  1. calculateUsersetNodeWeightWhenCycle orchestrates the process
  2. updateUsersetCycleWeight propagates weights throughout the cycle
  3. findUsersetWeightInCycle discovers weights within cycle paths

The logic correctly differentiates between edges that are part of cycles (returning Infinite) and non-cycle edges (using regular calculation).


996-1042: Strategy methods correctly implement operator semantics.

The three strategy methods properly reflect the semantics of different operators:

  • Max strategy (union, direct grouping): Succeeds if any branch reaches the userset
  • Max with enforcement (intersection): Succeeds only if all branches reach the userset
  • Hybrid strategy (exclusion): Handles "A but not B" by ensuring A reaches the userset and considering B's impact

1044-1072: Pruning logic correctly identifies edges that cannot contribute to userset paths.

The canPruneEdge method implements effective pruning by:

  1. Checking if the edge reaches the terminal types required by the userset
  2. Verifying that the edge's reach is sufficient (considering the +1 hop cost for Direct/TTU edges)
  3. Handling the special case of Infinite weights (recursive/cycle paths)

This prevents unnecessary traversal of edges that cannot possibly reach the target userset.


1074-1082: LGTM! Clean setter utilities for memoization.

The setter methods provide a consistent way to update memoization maps while returning the weight value, simplifying the calling code.

Comment thread pkg/go/graph/weighted_graph_userset_test.go Outdated
Comment thread pkg/go/graph/weighted_graph.go Outdated
Comment thread pkg/go/graph/weighted_graph.go Outdated
Comment thread pkg/go/graph/weighted_graph.go Outdated
Comment thread pkg/go/graph/weighted_graph.go
Comment thread pkg/go/graph/weighted_graph.go
Comment thread pkg/go/graph/weighted_graph.go
Comment thread pkg/go/graph/weighted_graph.go Outdated
Comment thread pkg/go/graph/weighted_graph.go
Comment thread pkg/go/graph/weighted_graph.go
Comment thread pkg/go/graph/weighted_graph.go Outdated
Comment thread pkg/go/graph/weighted_graph.go Outdated
elbuo8
elbuo8 previously approved these changes Oct 20, 2025
@adriantam
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 20, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
pkg/go/graph/weighted_graph.go (2)

153-166: Document the boolean return value

The function follows Go's getter pattern where the boolean indicates whether the weight exists, but this isn't documented. Consider adding a more detailed comment explaining what the boolean represents.

Apply this diff:

-// GetWeight returns the weight for the given key in the node, it could be a userset key or a terminal type key
+// GetWeight returns the weight for the given key in the node and a boolean indicating whether the weight exists.
+// The key can be either a userset key (containing "#") or a terminal type key.
 func (wg *WeightedAuthorizationModelGraph) GetWeight(node *WeightedAuthorizationModelNode, key string) (int, bool) {

203-214: Early pruning logic is correct but could use clearer comments

The three pruning conditions are:

  1. !exists: Node lacks a weight for a terminal type that the userset requires → no path possible
  2. nodeValue < usersetValue: Node's distance to terminal is shorter than userset's → node is "upstream" and can't reach userset
  3. nodeValue == usersetValue && nodeValue != Infinite: Equal distances but not infinite → would need +1 edge weight increment to reach, not possible with equal weights

Consider expanding the inline comments to explain this reasoning more clearly for future maintainers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a08501 and ee4cd11.

📒 Files selected for processing (1)
  • pkg/go/graph/weighted_graph.go (7 hunks)
🔇 Additional comments (6)
pkg/go/graph/weighted_graph.go (6)

57-57: LGTM: Consistent initialization of usersetWeights

The usersetWeights map is correctly initialized across all node and edge creation paths.

Also applies to: 69-69, 79-79, 106-106


805-899: Complex but well-structured recursive logic

The recursive weight calculation correctly handles:

  • Memoization to avoid redundant computation
  • Cycle detection via visited tracking (with separate handling for tuple cycles)
  • Early pruning via canPruneEdge
  • Incremental weight calculation for DirectEdge/TTUEdge
  • Multiple aggregation strategies based on node type

The comments at lines 812-814 helpfully explain the visited vs. cycle handling distinction.


906-998: Cycle handling logic is intricate but appears correct

The three-function cycle handling approach:

  1. calculateUsersetNodeWeightWhenCycle: Entry point for cycle nodes
  2. findUsersetWeightInCycle: Searches for userset within/from cycle, returns Infinite if found
  3. updateUsersetCycleWeight: Propagates the Infinite weight to all cycle nodes

The decision to return Infinite for any node in a cycle that can reach the userset is conservative but correct for pruning purposes—you cannot determine a finite distance from within a cycle.


1065-1080: Clarify the addOn logic in pruning condition

The pruning condition checks edgeWeight <= value + addOn where addOn is 1 for DirectEdge/TTUEdge and 0 otherwise. However, the comment at lines 1056-1059 states rules 2 and 3 suggest pruning when edgeWeight <= usersetWeight (for non-Infinite weights), which doesn't mention the addOn adjustment.

The early pruning in getWeightForUserset (line 210) checks nodeValue <= usersetValue without an addOn adjustment.

Can you clarify why addOn is included in the comparison? Is it accounting for the cost of traversing the edge from the current node to edge.to?


1086-1092: LGTM: Simple setter functions

These utility functions encapsulate the setting of userset weights on nodes and edges, providing a clear API for weight updates throughout the codebase.


1038-1054: Clarify or fix the exclusion weight calculation logic for usersets

In the hybrid strategy for exclusion (A but not B), the code returns weight2 (B's weight) when weight1 <= weight2 && weight2 != 0. This contradicts the expected semantics:

  • Terminal weights for exclusion (line 571-620): A's types are primary; overlapping B types use max weight
  • Userset weights should follow similar semantics: A's distance should be primary, not B's

Returning B's path distance for exclusion semantically inverts the operation. For example, if A reaches the userset in 2 steps and B reaches it in 3 steps, returning weight 3 misrepresents the reachability through the "A but not B" path.

Action: Either fix the logic to return weight1 (with appropriate B-based filtering), or add documentation and tests explaining why weight2 is semantically correct for exclusion.

adriantam
adriantam previously approved these changes Oct 20, 2025
@yissellokta yissellokta enabled auto-merge October 20, 2025 20:30
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@yissellokta yissellokta dismissed stale reviews from adriantam and elbuo8 via fd9ae1f October 20, 2025 20:34
@yissellokta yissellokta added this pull request to the merge queue Oct 20, 2025
Merged via the queue into main with commit c774550 Oct 20, 2025
10 checks passed
@yissellokta yissellokta deleted the feat/userset_weight_function branch October 20, 2025 21:12
@coderabbitai coderabbitai bot mentioned this pull request Nov 6, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants