Skip to content

[management] Affected peers for user updates - #7099

Merged
pascal-fischer merged 5 commits into
mainfrom
affected-peer-jwt-sync
Aug 7, 2026
Merged

[management] Affected peers for user updates#7099
pascal-fischer merged 5 commits into
mainfrom
affected-peer-jwt-sync

Conversation

@pascal-fischer

@pascal-fischer pascal-fischer commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • I ran and tested this change locally — I did not rely on CI to find out whether it works
  • This PR has a single purpose (not a fix + refactor + feature in one)
  • This change is a trivial fix, OR it links an issue the NetBird team agreed on beforehand. Changes to the public API, gRPC protocols, functionality behavior, CLI / service flags, or new features always need that agreement first. See CONTRIBUTING.md.

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • Improvements
    • Group membership and allowed-user changes now update only affected peers and SSH policy destinations, reducing unnecessary network updates.
    • JWT group additions and removals now reliably refresh associated users and policy-linked peers.
    • IPv6-related group changes trigger a complete peer update when address reassignment may affect connectivity.
  • Bug Fixes
    • Improved synchronization of SSH access rules when authorized groups or account-wide allowed users change.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: efcab0fd-6850-4393-9e4d-1d9f38b8b1aa

📥 Commits

Reviewing files that changed from the base of the PR and between d578113 and e10e392.

📒 Files selected for processing (3)
  • management/server/account.go
  • management/server/affected_peers_user_test.go
  • management/server/user.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • management/server/account.go
  • management/server/affected_peers_user_test.go
  • management/server/user.go

📝 Walkthrough

Walkthrough

JWT group and user updates now calculate affected peers from group membership, allowed-user, policy, router, resource, and peer snapshots. Updates target impacted peers asynchronously. IPv6 reconciliation still triggers account-wide propagation.

Changes

Affected-peer update flow

Layer / File(s) Summary
Affected-peer change resolution
management/server/affectedpeers/resolver.go, management/server/affectedpeers/resolver_test.go
The resolver handles user-group and allowed-user changes. It refreshes destinations for matching SSH rules and validates the new change fields.
User update effect tracking
management/server/user.go, management/server/affected_peers_user_test.go
User updates return structured affected-peer changes. Tests cover targeted updates, unchanged memberships, IPv6 reassignment, and user unblocking.
JWT synchronization dispatch
management/server/account.go, management/server/affected_peers_jwt_test.go
JWT synchronization records group and link changes, loads snapshots transactionally, and dispatches targeted updates. Tests cover SSH policy impact and JWT group additions and removals.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UserUpdate
  participant AffectedPeerResolver
  participant PeerUpdateDispatcher
  participant Peer
  UserUpdate->>AffectedPeerResolver: Resolve changed groups and allowed users
  AffectedPeerResolver-->>UserUpdate: Return affected peers
  UserUpdate->>PeerUpdateDispatcher: Dispatch targeted update
  PeerUpdateDispatcher->>Peer: Send network map update
Loading

Suggested reviewers: jnfrati

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description follows the template but lacks change details, the required issue link, checklist confirmations, and an explanation for why documentation is not needed. Add implementation details, link the approved issue or discussion, complete applicable checklist items, and explain why documentation is not needed.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: targeted affected-peer updates for user changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch affected-peer-jwt-sync

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.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Release artifacts

Built for PR head e10e392 in workflow run #17585.

Artifact Link
All release artifacts Download
Linux packages Download
Windows packages Download
macOS packages Download
UI artifacts Download
UI GTK3 artifacts Download
UI macOS artifacts Download

GHCR images (amd64)

This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
management/server/account.go (1)

2445-2456: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Two settings reads can disagree about IPv6 reconciliation.

SyncUserJWTGroups calls ipv6ReconcileNeeded with the settings loaded before the transaction (Line 1701). reconcileIPv6ForGroupChanges re-reads settings inside the transaction and evaluates the same predicate again (Lines 2446-2453). If IPv6EnabledGroups changes between the two reads, the reconciliation runs while requiresAccountUpdate stays false, so the account-wide update is skipped and peers keep stale IPv6 addresses. Return the decision from reconcileIPv6ForGroupChanges, or pass the transaction-scoped settings to both call sites.

🔧 Proposed direction
-func (am *DefaultAccountManager) reconcileIPv6ForGroupChanges(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) error {
+// reconcileIPv6ForGroupChanges reports whether it reassigned addresses, so callers that
+// compute an affected-peers set can fall back to an account-wide update.
+func (am *DefaultAccountManager) reconcileIPv6ForGroupChanges(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) (bool, error) {
 	settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
 	if err != nil {
-		return fmt.Errorf("get account settings: %w", err)
+		return false, fmt.Errorf("get account settings: %w", err)
 	}
 
 	if !ipv6ReconcileNeeded(settings, groupIDs) {
-		return nil
+		return false, nil
 	}
 
-	return am.updatePeerIPv6Addresses(ctx, transaction, accountID, settings)
+	return true, am.updatePeerIPv6Addresses(ctx, transaction, accountID, settings)
 }

Then set requiresAccountUpdate from that return value in SyncUserJWTGroups and in processUserUpdate, and drop the separate ipv6ReconcileNeeded call at both sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/account.go` around lines 2445 - 2456, Make the
transaction-scoped IPv6 reconciliation decision authoritative: update
reconcileIPv6ForGroupChanges to return whether ipv6ReconcileNeeded was true,
then use that result to set requiresAccountUpdate in SyncUserJWTGroups and
processUserUpdate. Remove the separate ipv6ReconcileNeeded calls at both callers
while preserving the existing account update and peer-address update behavior.
🧹 Nitpick comments (2)
management/server/user.go (2)

887-896: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename allGroupIDs to state that it returns the All group.

The plural name reads as "the IDs of all groups". The function returns only the account's All group ID. Rename it to allGroupID or groupAllIDs to prevent a future caller from treating the result as every group.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/user.go` around lines 887 - 896, Rename the allGroupIDs
function to singularly indicate that it returns the account’s All group ID, such
as allGroupID, and update every call site and related references consistently
while preserving its slice return type and behavior.

646-649: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unwrapped affectedpeers.Load errors in both transaction callbacks. Both new snapshot loads return the store error without context, while every other error in the same callback is wrapped. A failure therefore reaches the caller log without identifying the failing step.

  • management/server/user.go#L646-L649: wrap the error, for example fmt.Errorf("load affected peers for user %s: %w", update.Id, err).
  • management/server/account.go#L1712-L1715: wrap the error, for example fmt.Errorf("load affected peers: %w", err).

As per coding guidelines: "add concise error context without prefixes such as failed to or error".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/user.go` around lines 646 - 649, Wrap the errors returned
by affectedpeers.Load in both transaction callbacks with concise context using
%w, without prefixes such as “failed to” or “error”. Update
management/server/user.go:646-649 in the user update callback to identify the
affected user, and management/server/account.go:1712-1715 in the account
callback to identify the affected-peers loading step.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@management/server/affectedpeers/resolver.go`:
- Around line 878-883: The ruleShipsAllowedUsers logic must classify NetBird SSH
rules with AuthorizedUser as allowed-user rules so destination peers refresh
when AllowedUsersChanged is true. Update the NetBird SSH branch to match
AuthorizedUser rules appropriately, while preserving the existing empty
AuthorizedGroups and empty AuthorizedUser condition for rules without a named
user.

In `@management/server/user.go`:
- Line 849: Update SaveOrAddUsers to handle a nil settings result from
GetAccountSettings before evaluating settings.GroupsPropagationEnabled. Preserve
existing store-error handling, and either guard the dereference or apply the
established validation defaults for missing account settings.

---

Outside diff comments:
In `@management/server/account.go`:
- Around line 2445-2456: Make the transaction-scoped IPv6 reconciliation
decision authoritative: update reconcileIPv6ForGroupChanges to return whether
ipv6ReconcileNeeded was true, then use that result to set requiresAccountUpdate
in SyncUserJWTGroups and processUserUpdate. Remove the separate
ipv6ReconcileNeeded calls at both callers while preserving the existing account
update and peer-address update behavior.

---

Nitpick comments:
In `@management/server/user.go`:
- Around line 887-896: Rename the allGroupIDs function to singularly indicate
that it returns the account’s All group ID, such as allGroupID, and update every
call site and related references consistently while preserving its slice return
type and behavior.
- Around line 646-649: Wrap the errors returned by affectedpeers.Load in both
transaction callbacks with concise context using %w, without prefixes such as
“failed to” or “error”. Update management/server/user.go:646-649 in the user
update callback to identify the affected user, and
management/server/account.go:1712-1715 in the account callback to identify the
affected-peers loading step.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 554f5863-67d3-401c-aab3-1f63ebf961cc

📥 Commits

Reviewing files that changed from the base of the PR and between 524b8b9 and c07453d.

📒 Files selected for processing (5)
  • management/server/account.go
  • management/server/affected_peers_jwt_test.go
  • management/server/affectedpeers/resolver.go
  • management/server/affectedpeers/resolver_test.go
  • management/server/user.go

Comment on lines +878 to +883
func ruleShipsAllowedUsers(rule *types.PolicyRule) bool {
if rule.Protocol == types.PolicyRuleProtocolNetbirdSSH {
return len(rule.AuthorizedGroups) == 0 && rule.AuthorizedUser == ""
}
return types.PolicyRuleImpliesLegacySSH(rule)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how AuthorizedUser is applied in the network map and whether blocked users are filtered.
set -euo pipefail

rg -n -C 8 'AuthorizedUser\b' --type=go -g '!**/*_test.go'
rg -n -C 6 'IsBlocked\(\)' --type=go -g '**/networkmap/**' -g '**/network_map/**' -g '!**/*_test.go'

Repository: netbirdio/netbird

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked files around resolver:"
git ls-files | rg 'management/server/affectedpeers/resolver\.go|policy|routers|networkmap|network_map' | head -200

echo
echo "Resolver lines 800-920:"
if [ -f management/server/affectedpeers/resolver.go ]; then
  sed -n '800,920p' management/server/affectedpeers/resolver.go | nl -ba -v800
fi

echo
echo "Search AuthorizedUser (no non-test Go files removed):"
rg -n -C 8 'AuthorizedUser\b' --glob '*.go' | head -300 || true

echo
echo "Search IsBlocked in Go files:"
rg -n -C 6 'IsBlocked\(\)' --glob '*.go' | head -400 || true

echo
echo "Search PolicyRuleImpliesLegacySSH/netbird ssh:"
rg -n -C 6 'PolicyRuleImpliesLegacySSH|PolicyRuleProtocolNetbirdSSH|NetbirdSSH|netbirdssh' --glob '*.go' | head -500 || true

echo
echo "Locate types files:"
git ls-files | rg '(^|/)types\.go$|policy\.go$|peers\.go$|rules' | head -200

Repository: netbirdio/netbird

Length of output: 3129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Resolver lines 800-920:"
sed -n '800,920p' management/server/affectedpeers/resolver.go | cat -n -v

echo
echo "Search AuthorizedUser in tracked Go files:"
rg -n -C 8 'AuthorizedUser\b' "${PWD}" -g '*.go' | head -400 || true

echo
echo "Search IsBlocked in tracked Go files:"
rg -n -C 6 'IsBlocked\(\)' "${PWD}" -g '*.go' | head -400 || true

echo
echo "Search PolicyRule protocols and helpers in tracked Go files:"
rg -n -C 6 'PolicyRuleImpliesLegacySSH|PolicyRuleProtocolNetbirdSSH|NetbirdSSH|netbirdssh|CollectFromSSHAuthorizedGroups|ruleShipsAllowedUsers|collectFromAllowedUsers' "${PWD}" -g '*.go' || true

Repository: netbirdio/netbird

Length of output: 50374


Include AuthorizedUser rules in ruleShipsAllowedUsers.

For NetBird SSH, a rule with AuthorizedUser still has its destination peers carry the account's allowed-user set (allowedUsers[auth.Wildcard][rule.AuthorizedUser]). The current fallback treats it as legacy SSH, so destination peers are not refreshed when AllowedUsersChanged is true. Match AuthorizedUser here instead: len(rule.AuthorizedGroups) == 0 && rule.AuthorizedUser == "" stays false for named-user rules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/affectedpeers/resolver.go` around lines 878 - 883, The
ruleShipsAllowedUsers logic must classify NetBird SSH rules with AuthorizedUser
as allowed-user rules so destination peers refresh when AllowedUsersChanged is
true. Update the NetBird SSH branch to match AuthorizedUser rules appropriately,
while preserving the existing empty AuthorizedGroups and empty AuthorizedUser
condition for rules without a named user.

Comment thread management/server/user.go
}

var removedGroups, addedGroups []string
if update.AutoGroups != nil && settings.GroupsPropagationEnabled {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether GetAccountSettings can return (nil, nil) and how other callers guard it.
set -euo pipefail

ast-grep run --pattern 'func (s *SqlStore) GetAccountSettings($$$) ($$$) { $$$ }' --lang go
rg -n -C 3 'GetAccountSettings\(' --type=go -g '!**/*_test.go' -g '!**/*_mock.go'

Repository: netbirdio/netbird

Length of output: 1454


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | grep -E '(^|/)user\.go$|(^|/)account\.go$|(^|/)sql_store\.go$' || true

echo "== processUserUpdate and SaveOrAddUsers call =="
rg -n -C 8 'func processUserUpdate|processUserUpdate\(|func SaveOrAddUsers|SaveOrAddUsers\(' management/server/user.go management/server/account.go || true

echo "== GetAccountSettings callers excluding mocks/tests =="
rg -n -C 4 'GetAccountSettings\(' --type=go -g '!**/*_test.go' -g '!**/*_mock.go' || true

echo "== user.go lines around processUserUpdate =="
wc -l management/server/user.go
sed -n '800,870p' management/server/user.go

echo "== sql_store GetAccountSettings implementation =="
sed -n '2938,2970p' management/server/store/sql_store.go

Repository: netbirdio/netbird

Length of output: 5744


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SaveOrAddUsers full relevant lines =="
sed -n '579,650p' management/server/user.go

echo "== processUserUpdate dereferences settings =="
sed -n '782,870p' management/server/user.go

echo [=[
== AccountSettings nil return and validation helpers/defaults if present
]=]
rg -n -C 3 'Validate.*Settings|Settings.*Validation|Settings.*Default|default_settings|GroupsPropagationEnabled|GetAccountSettings' management/server --type=go -g '!**_*test.go' -g '!**_mock.go' || true

echo "== exact GetAccountSettings implementation =="
sed -n '2946,2966p' management/server/store/sql_store.go

echo "== call sites by searching broader references =="
rg -n 'GetAccountSettings|settings GroupsPropagationEnabled|settings.*GroupsPropagation' --type=go -g '!**/*_test.go' -g '!**/*_mock.go' || true

Repository: netbirdio/netbird

Length of output: 6374


Handle missing account settings before dereferencing settings.

SaveOrAddUsers returns on store errors from GetAccountSettings, but a missing settings row can still return (nil, nil) and hit settings.GroupsPropagationEnabled. Add nil handling before this dereference or route missing settings through validation defaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/user.go` at line 849, Update SaveOrAddUsers to handle a nil
settings result from GetAccountSettings before evaluating
settings.GroupsPropagationEnabled. Preserve existing store-error handling, and
either guard the dereference or apply the established validation defaults for
missing account settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
management/server/affected_peers_user_test.go (2)

155-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the expected result for updTarget.

Line 155 drains updTarget, but no assertion follows for that channel. The unblocked user owns targetPeer, so its expected behavior is the most relevant outcome of this subtest. Add either peerShouldReceiveUpdate(t, updTarget) or peerShouldNotReceiveUpdate(t, updTarget) to record the intended contract.

💚 Proposed addition
+		peerShouldReceiveUpdate(t, updTarget)
 		peerShouldReceiveUpdate(t, upd2)
 		peerShouldNotReceiveUpdate(t, upd3)

Select the assertion that matches the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/affected_peers_user_test.go` around lines 155 - 168, Add an
explicit assertion for updTarget after SaveUser in the affected subtest, using
peerShouldReceiveUpdate or peerShouldNotReceiveUpdate according to the intended
unblock-refresh contract; keep the existing upd2 and upd3 assertions unchanged.

110-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the required subtest order.

These subtests share one account, one peer set, and one channel set. Each subtest depends on the state that the previous subtest leaves behind. Subtest 4 depends on IPv6EnabledGroups and the auto-groups set here. A future t.Parallel() call or a reordering breaks the test in a way that is hard to diagnose. Add a short comment at the top of the test that states the subtests run in order and must not run in parallel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@management/server/affected_peers_user_test.go` around lines 110 - 132, Add a
short comment at the beginning of the parent test containing these subtests,
before the first t.Run call, stating that subtests share state, must execute in
order, and must not run in parallel. Use the surrounding test function as the
insertion point and leave the subtest logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@management/server/affected_peers_user_test.go`:
- Around line 155-168: Add an explicit assertion for updTarget after SaveUser in
the affected subtest, using peerShouldReceiveUpdate or
peerShouldNotReceiveUpdate according to the intended unblock-refresh contract;
keep the existing upd2 and upd3 assertions unchanged.
- Around line 110-132: Add a short comment at the beginning of the parent test
containing these subtests, before the first t.Run call, stating that subtests
share state, must execute in order, and must not run in parallel. Use the
surrounding test function as the insertion point and leave the subtest logic
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b8621f19-6b53-454c-b594-e644d1593022

📥 Commits

Reviewing files that changed from the base of the PR and between c07453d and d578113.

📒 Files selected for processing (1)
  • management/server/affected_peers_user_test.go

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
2 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@pascal-fischer
pascal-fischer merged commit 2ee21d2 into main Aug 7, 2026
47 checks passed
@pascal-fischer
pascal-fischer deleted the affected-peer-jwt-sync branch August 7, 2026 16:07
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.

2 participants