Skip to content

[proxy] Send proxy updates on account delete#5375

Open
pascal-fischer wants to merge 9 commits intomainfrom
fix/send-proxy-updates-on-account-delete
Open

[proxy] Send proxy updates on account delete#5375
pascal-fischer wants to merge 9 commits intomainfrom
fix/send-proxy-updates-on-account-delete

Conversation

@pascal-fischer
Copy link
Collaborator

@pascal-fischer pascal-fischer commented Feb 18, 2026

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)

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

  • New Features

    • Delete all services for an account in a single operation.
    • Account deletion now triggers cleanup of associated remote services.
  • Performance / Efficiency

    • Batch processing of service updates for more efficient cross-cluster synchronization.
  • Refactor

    • Reworked proxy update messaging and per-proxy token handling for more consistent, scalable delivery.
  • Tests

    • Updated proxy streaming tests to match the new message flow.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Manager.DeleteAllServices and store support to fetch all account services, converts reverse-proxy streaming messages from ProxyMapping to GetMappingUpdateResponse, introduces per-cluster batched emission of mapping updates across create/update/delete/reload flows, and invokes bulk remote-service cleanup during account deletion.

Changes

Cohort / File(s) Summary
Reverse Proxy Interface & Manager
management/internals/modules/reverseproxy/interface.go, management/internals/modules/reverseproxy/manager/manager.go
Added DeleteAllServices(ctx, accountID, userID) to Manager interface and mocks; manager now builds per-cluster mapping batches and dispatches them via new helpers sendServiceUpdate and sendMappingsToCluster for create/update/delete/reload flows.
gRPC Proxy Streaming & Tests
management/internals/shared/grpc/proxy.go, management/internals/shared/grpc/proxy_test.go, management/internals/shared/grpc/proxy_group_access_test.go
Changed channels and function signatures to use *proto.GetMappingUpdateResponse instead of *proto.ProxyMapping; updated per-proxy token generation to attach tokens inside GetMappingUpdateResponse.Mapping[] and adjusted tests accordingly.
Store Interface & SQL Implementation
management/server/store/store.go, management/server/store/sql_store.go, management/server/store/store_mock.go
Added GetServicesByAccountID(ctx, lockStrength, accountID) to Store interface, implemented in SqlStore (Preload Targets, optional locking, decrypt per-service), and added corresponding mock methods.
Account Deletion Flow
management/server/account.go, management/server/account_test.go
DeleteAccount now calls reverseProxyManager.DeleteAllServices(ctx, accountID, userID) before removing users; tests updated to wire a proxy gRPC server into the manager setup.
Mocks & Tests Integration
management/internals/modules/reverseproxy/interface_mock.go, proxy/management_integration_test.go
Added mock and test no-op implementations for DeleteAllServices to satisfy the extended Manager interface; recorder updated for expectations.

Sequence Diagram(s)

sequenceDiagram
    participant M as Manager
    participant S as ProxyServiceServer
    participant C as Cluster
    participant P as Proxy

    M->>M: gather services grouped by cluster
    M->>S: Send GetMappingUpdateResponse (per-cluster batch)
    S->>S: perProxyMessage — generate per-proxy tokens, build Mapping[]
    S->>C: deliver GetMappingUpdateResponse to cluster
    C->>P: distribute mapping update to proxies
    P-->>C: ack
    C-->>S: ack
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • crn4

Poem

🐰 I hopped through code and grouped each mapping flock,

Batched by cluster, tokens snug in every sock.
When accounts vanish, I tidy every trail,
Quiet rabbit sweep—no mapping left to mail. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. Critical sections are missing: 'Describe your changes' is empty, no issue ticket number/link provided, and no explanation for why documentation is not needed. Fill in the 'Describe your changes' section with a detailed explanation of the implementation. Add the issue ticket number and link. Explain why documentation updates are not required for this feature enhancement.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding proxy update sending functionality when accounts are deleted, which aligns with the code changes introducing DeleteAllServices calls and batched proxy updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/send-proxy-updates-on-account-delete

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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
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: 4

🧹 Nitpick comments (5)
management/server/store/store.go (1)

259-259: Consider consolidating with GetAccountServices to avoid API duplication.

GetServicesByAccountID appears to overlap with the existing GetAccountServices signature. If both are required, add a brief doc comment clarifying the behavioral difference; otherwise, consider aliasing or deprecating one to keep the interface surface minimal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/store/store.go` at line 259, GetServicesByAccountID
overlaps GetAccountServices; either consolidate by removing one and making the
remaining function serve both callers (rename callers to use GetAccountServices
or add an alias that calls GetAccountServices), or if both must exist, add a
clear doc comment on GetServicesByAccountID explaining the behavioral difference
(locking behavior, return values, or error semantics) relative to
GetAccountServices; update any callers to use the canonical function (or the
alias) and ensure tests reflect the chosen consolidation.
management/server/store/sql_store.go (1)

4909-4929: Deduplicate service-loading logic to avoid drift.

This implementation mirrors GetAccountServices. Delegating keeps behavior consistent and reduces maintenance.

♻️ Suggested refactor
 func (s *SqlStore) GetServicesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {
-	tx := s.db.Preload("Targets")
-	if lockStrength != LockingStrengthNone {
-		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
-	}
-
-	var serviceList []*reverseproxy.Service
-	result := tx.Find(&serviceList, accountIDCondition, accountID)
-	if result.Error != nil {
-		log.WithContext(ctx).Errorf("failed to get services from the store: %s", result.Error)
-		return nil, status.Errorf(status.Internal, "failed to get services from store")
-	}
-
-	for _, service := range serviceList {
-		if err := service.DecryptSensitiveData(s.fieldEncrypt); err != nil {
-			return nil, fmt.Errorf("decrypt service data: %w", err)
-		}
-	}
-
-	return serviceList, nil
+	return s.GetAccountServices(ctx, lockStrength, accountID)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/store/sql_store.go` around lines 4909 - 4929,
GetServicesByAccountID duplicates the logic in GetAccountServices; refactor it
to delegate to the existing implementation to avoid drift. Replace the body of
GetServicesByAccountID so it simply calls s.GetAccountServices(ctx,
lockStrength, accountID) (or the appropriate exported/internal signature) and
returns its results, preserving the same LockingStrength and accountID
parameters and propagating any errors; ensure you remove the duplicated DB
Preload/Find and decryption logic from GetServicesByAccountID so all
service-loading behavior lives in GetAccountServices.
management/internals/modules/reverseproxy/manager/manager.go (2)

347-351: oldService parameter in sendServiceUpdate is misleading and always "".

The second positional argument to service.ToProtoMapping is an auth token, not an old service ID (compare with proxy.go's snapshot path: service.ToProtoMapping(reverseproxy.Create, token, ...)). Every call site passes "" for oldService, making it a dead parameter with a name that implies a service-rename semantic.

Either drop the parameter and hard-code "", or rename it to token to match ToProtoMapping's actual contract:

♻️ Proposed fix
-func (m *managerImpl) sendServiceUpdate(service *reverseproxy.Service, operation reverseproxy.Operation, cluster, oldService string) {
+func (m *managerImpl) sendServiceUpdate(service *reverseproxy.Service, operation reverseproxy.Operation, cluster string) {
 	oidcCfg := m.proxyGRPCServer.GetOIDCValidationConfig()
-	mapping := service.ToProtoMapping(operation, oldService, oidcCfg)
+	mapping := service.ToProtoMapping(operation, "", oidcCfg)
 	m.sendMappingsToCluster([]*proto.ProxyMapping{mapping}, cluster)
 }

All call sites drop the trailing "" argument accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/modules/reverseproxy/manager/manager.go` around lines
347 - 351, The parameter named oldService on managerImpl.sendServiceUpdate is
misleading (ToProtoMapping expects an auth token) — rename the parameter to
token (and the local variable usage) so the signature becomes
sendServiceUpdate(service *reverseproxy.Service, operation
reverseproxy.Operation, cluster, token string) and update all call sites that
pass "" to pass token (or leave "" where appropriate); ensure the call to
service.ToProtoMapping(operation, token, oidcCfg) reflects the renamed parameter
and that any documentation/comments are updated to reflect it represents an auth
token, not an old service ID.

439-443: DeleteAllServices issues N individual DeleteService calls inside a single transaction — consider a bulk delete.

For accounts with many services, this results in N round-trips inside the transaction. A single WHERE account_id = ? delete query would be more efficient and reduces lock contention. The GetServicesByAccountID → individual-delete pattern also means the lock is held for longer.

Would you like me to draft a DeleteAllServicesByAccountID(ctx, accountID) store method (and its mock counterpart) that executes a single bulk-delete query, removing the need for the loop here?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/modules/reverseproxy/manager/manager.go` around lines
439 - 443, DeleteAllServices currently fetches services then calls
transaction.DeleteService repeatedly, causing N round-trips and long locks;
replace this with a single bulk delete in the store. Add a new store method
DeleteAllServicesByAccountID(ctx, accountID) (and update its mock) that runs one
DELETE FROM ... WHERE account_id = ? query and returns any error, then modify
the manager's DeleteAllServices to call
transaction.DeleteAllServicesByAccountID(ctx, accountID) instead of looping over
GetServicesByAccountID/transaction.DeleteService to eliminate the per-service
loop and reduce lock contention.
management/internals/shared/grpc/proxy_test.go (1)

43-101: No test coverage for batched multi-mapping responses — the core new behavior.

All three test functions send a GetMappingUpdateResponse with exactly one ProxyMapping. The key behavioral change in this PR is that perProxyMessage now iterates the Mapping slice and generates a per-mapping, per-proxy token for every non-delete entry. A test with two or more non-delete mappings in a single response would verify that:

  1. Each mapping in the received response has a distinct token from the others.
  2. Tokens for the same mapping index across proxies are distinct.
  3. All generated tokens are independently consumable.

This is the logic path that DeleteAllServices and ReloadAllServicesForAccount now exercise in production.

Would you like me to generate a TestSendServiceUpdateToCluster_BatchedMappingsUniqueTokens test?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/shared/grpc/proxy_test.go` around lines 43 - 101, Add a
new unit test (e.g., TestSendServiceUpdateToCluster_BatchedMappingsUniqueTokens)
that uses NewOneTimeTokenStore and a ProxyServiceServer, registers multiple fake
proxies via registerFakeProxy, then sends a single GetMappingUpdateResponse
containing multiple non-delete ProxyMapping entries to
s.SendServiceUpdateToCluster; for each proxy received (via drainChannel) assert
each mapping has a non-empty distinct AuthToken within that proxy message and
that tokens for the same mapping index across different proxies are distinct,
then call tokenStore.ValidateAndConsume for every token+account+service
combination to ensure each token is independently consumable; reference
tokenStore, ProxyServiceServer, registerFakeProxy, SendServiceUpdateToCluster,
drainChannel, and ValidateAndConsume in the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@management/internals/shared/grpc/proxy.go`:
- Around line 457-479: In perProxyMessage, avoid aborting the whole batch on a
single token generation error: when tokenStore.GenerateToken(mapping.AccountId,
mapping.Id, 5*time.Minute) fails, log the error and skip that mapping (do not
return nil), continue processing the rest, and only return nil if the resulting
resp slice is empty; keep using shallowCloneMapping to copy entries and set
msg.AuthToken for successful tokens so
SendServiceUpdate/SendServiceUpdateToCluster still receives partial batches
rather than discarding all mappings.
- Line 65: Remove the unused field updatesChan from the proxy server struct and
its initialization in NewProxyServiceServer; specifically, delete the
updatesChan field declaration and the code that allocates it (since
SendServiceUpdate and SendServiceUpdateToCluster already write to each
conn.sendChan and the sender goroutine consumes conn.sendChan). Ensure no other
code references updatesChan and run tests/build to confirm removal is safe; use
symbol names updatesChan, NewProxyServiceServer, SendServiceUpdate,
SendServiceUpdateToCluster, conn.sendChan, and sender to locate the related
code.

In `@management/server/account.go`:
- Around line 717-720: Guard against a nil reverseProxyManager before calling
DeleteAllServices: update the account deletion flow (where
am.reverseProxyManager.DeleteAllServices(ctx, accountID, userID) is called) to
first check if am.reverseProxyManager != nil and only call DeleteAllServices
when configured; if nil, skip the call. Also fix the error message returned from
status.Errorf to accurately describe the operation (e.g., "failed to delete
reverse proxy services for account %s: %v") and keep using accountID and err for
context.

In `@management/server/store/store_mock.go`:
- Around line 1097-1110: Duplicate API methods GetServicesByAccountID and
GetAccountServices exist; remove the redundant GetServicesByAccountID
implementation and its mock helpers to consolidate on a single method name
(choose one, e.g., GetAccountServices). Update the Store interface to only
declare the chosen method, delete MockStore.GetServicesByAccountID and
MockStoreMockRecorder.GetServicesByAccountID from store_mock.go, and update any
callers/tests to call the remaining method (GetAccountServices) and regenerate
mocks so gomock reflect types remain consistent.

---

Nitpick comments:
In `@management/internals/modules/reverseproxy/manager/manager.go`:
- Around line 347-351: The parameter named oldService on
managerImpl.sendServiceUpdate is misleading (ToProtoMapping expects an auth
token) — rename the parameter to token (and the local variable usage) so the
signature becomes sendServiceUpdate(service *reverseproxy.Service, operation
reverseproxy.Operation, cluster, token string) and update all call sites that
pass "" to pass token (or leave "" where appropriate); ensure the call to
service.ToProtoMapping(operation, token, oidcCfg) reflects the renamed parameter
and that any documentation/comments are updated to reflect it represents an auth
token, not an old service ID.
- Around line 439-443: DeleteAllServices currently fetches services then calls
transaction.DeleteService repeatedly, causing N round-trips and long locks;
replace this with a single bulk delete in the store. Add a new store method
DeleteAllServicesByAccountID(ctx, accountID) (and update its mock) that runs one
DELETE FROM ... WHERE account_id = ? query and returns any error, then modify
the manager's DeleteAllServices to call
transaction.DeleteAllServicesByAccountID(ctx, accountID) instead of looping over
GetServicesByAccountID/transaction.DeleteService to eliminate the per-service
loop and reduce lock contention.

In `@management/internals/shared/grpc/proxy_test.go`:
- Around line 43-101: Add a new unit test (e.g.,
TestSendServiceUpdateToCluster_BatchedMappingsUniqueTokens) that uses
NewOneTimeTokenStore and a ProxyServiceServer, registers multiple fake proxies
via registerFakeProxy, then sends a single GetMappingUpdateResponse containing
multiple non-delete ProxyMapping entries to s.SendServiceUpdateToCluster; for
each proxy received (via drainChannel) assert each mapping has a non-empty
distinct AuthToken within that proxy message and that tokens for the same
mapping index across different proxies are distinct, then call
tokenStore.ValidateAndConsume for every token+account+service combination to
ensure each token is independently consumable; reference tokenStore,
ProxyServiceServer, registerFakeProxy, SendServiceUpdateToCluster, drainChannel,
and ValidateAndConsume in the test.

In `@management/server/store/sql_store.go`:
- Around line 4909-4929: GetServicesByAccountID duplicates the logic in
GetAccountServices; refactor it to delegate to the existing implementation to
avoid drift. Replace the body of GetServicesByAccountID so it simply calls
s.GetAccountServices(ctx, lockStrength, accountID) (or the appropriate
exported/internal signature) and returns its results, preserving the same
LockingStrength and accountID parameters and propagating any errors; ensure you
remove the duplicated DB Preload/Find and decryption logic from
GetServicesByAccountID so all service-loading behavior lives in
GetAccountServices.

In `@management/server/store/store.go`:
- Line 259: GetServicesByAccountID overlaps GetAccountServices; either
consolidate by removing one and making the remaining function serve both callers
(rename callers to use GetAccountServices or add an alias that calls
GetAccountServices), or if both must exist, add a clear doc comment on
GetServicesByAccountID explaining the behavioral difference (locking behavior,
return values, or error semantics) relative to GetAccountServices; update any
callers to use the canonical function (or the alias) and ensure tests reflect
the chosen consolidation.


// Channel for broadcasting reverse proxy updates to all proxies
updatesChan chan *proto.ProxyMapping
updatesChan chan *proto.GetMappingUpdateResponse
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "updatesChan" --type=go

Repository: netbirdio/netbird

Length of output: 271


🏁 Script executed:

sed -n '60,70p' management/internals/shared/grpc/proxy.go
sed -n '108,118p' management/internals/shared/grpc/proxy.go

Repository: netbirdio/netbird

Length of output: 942


Remove unused updatesChan field — dead code.

updatesChan is declared (line 65) and initialized (line 113) in NewProxyServiceServer, but is never written to or read from anywhere in the codebase. Both SendServiceUpdate and SendServiceUpdateToCluster write directly to each conn.sendChan; the sender goroutine reads from conn.sendChan. The field should be removed.

♻️ Proposed fix
 type ProxyServiceServer struct {
     proto.UnimplementedProxyServiceServer
     connectedProxies sync.Map
     clusterProxies   sync.Map
-    // Channel for broadcasting reverse proxy updates to all proxies
-    updatesChan chan *proto.GetMappingUpdateResponse
     // Manager for access logs
     accessLogManager accesslogs.Manager
 s := &ProxyServiceServer{
-    updatesChan:       make(chan *proto.GetMappingUpdateResponse, 100),
     accessLogManager:  accessLogMgr,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/shared/grpc/proxy.go` at line 65, Remove the unused
field updatesChan from the proxy server struct and its initialization in
NewProxyServiceServer; specifically, delete the updatesChan field declaration
and the code that allocates it (since SendServiceUpdate and
SendServiceUpdateToCluster already write to each conn.sendChan and the sender
goroutine consumes conn.sendChan). Ensure no other code references updatesChan
and run tests/build to confirm removal is safe; use symbol names updatesChan,
NewProxyServiceServer, SendServiceUpdate, SendServiceUpdateToCluster,
conn.sendChan, and sender to locate the related code.

Comment on lines +457 to 479
func (s *ProxyServiceServer) perProxyMessage(update *proto.GetMappingUpdateResponse, proxyID string) *proto.GetMappingUpdateResponse {
resp := make([]*proto.ProxyMapping, 0, len(update.Mapping))
for _, mapping := range update.Mapping {
if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
resp = append(resp, mapping)
continue
}

token, err := s.tokenStore.GenerateToken(update.AccountId, update.Id, 5*time.Minute)
if err != nil {
log.Warnf("Failed to generate token for proxy %s: %v", proxyID, err)
return nil
token, err := s.tokenStore.GenerateToken(mapping.AccountId, mapping.Id, 5*time.Minute)
if err != nil {
log.Warnf("Failed to generate token for proxy %s: %v", proxyID, err)
return nil
}

msg := shallowCloneMapping(mapping)
msg.AuthToken = token
resp = append(resp, msg)
}

msg := shallowCloneMapping(update)
msg.AuthToken = token
return msg
return &proto.GetMappingUpdateResponse{
Mapping: resp,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

perProxyMessage: a single token-generation failure silently drops the entire batch for that proxy.

The early return nil on line 468 causes SendServiceUpdate/SendServiceUpdateToCluster to skip the proxy entirely, discarding all other mappings in the batch that may have been processed successfully. Before this PR, each mapping was dispatched in its own GetMappingUpdateResponse, so a failure only lost one service update. With batching (e.g. DeleteAllServices deletes N services in one shot), a transient token-store error now silently loses all N delete notifications for one proxy.

Consider continuing to build the response with successfully tokenized mappings and only skipping the failed entry, returning nil only when resp ends up empty:

🛡️ Proposed fix
 func (s *ProxyServiceServer) perProxyMessage(update *proto.GetMappingUpdateResponse, proxyID string) *proto.GetMappingUpdateResponse {
 	resp := make([]*proto.ProxyMapping, 0, len(update.Mapping))
 	for _, mapping := range update.Mapping {
 		if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
 			resp = append(resp, mapping)
 			continue
 		}

 		token, err := s.tokenStore.GenerateToken(mapping.AccountId, mapping.Id, 5*time.Minute)
 		if err != nil {
 			log.Warnf("Failed to generate token for proxy %s mapping %s: %v", proxyID, mapping.Id, err)
-			return nil
+			continue
 		}

 		msg := shallowCloneMapping(mapping)
 		msg.AuthToken = token
 		resp = append(resp, msg)
 	}

+	if len(resp) == 0 {
+		return nil
+	}
 	return &proto.GetMappingUpdateResponse{
 		Mapping: resp,
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *ProxyServiceServer) perProxyMessage(update *proto.GetMappingUpdateResponse, proxyID string) *proto.GetMappingUpdateResponse {
resp := make([]*proto.ProxyMapping, 0, len(update.Mapping))
for _, mapping := range update.Mapping {
if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
resp = append(resp, mapping)
continue
}
token, err := s.tokenStore.GenerateToken(update.AccountId, update.Id, 5*time.Minute)
if err != nil {
log.Warnf("Failed to generate token for proxy %s: %v", proxyID, err)
return nil
token, err := s.tokenStore.GenerateToken(mapping.AccountId, mapping.Id, 5*time.Minute)
if err != nil {
log.Warnf("Failed to generate token for proxy %s: %v", proxyID, err)
return nil
}
msg := shallowCloneMapping(mapping)
msg.AuthToken = token
resp = append(resp, msg)
}
msg := shallowCloneMapping(update)
msg.AuthToken = token
return msg
return &proto.GetMappingUpdateResponse{
Mapping: resp,
}
}
func (s *ProxyServiceServer) perProxyMessage(update *proto.GetMappingUpdateResponse, proxyID string) *proto.GetMappingUpdateResponse {
resp := make([]*proto.ProxyMapping, 0, len(update.Mapping))
for _, mapping := range update.Mapping {
if mapping.Type == proto.ProxyMappingUpdateType_UPDATE_TYPE_REMOVED {
resp = append(resp, mapping)
continue
}
token, err := s.tokenStore.GenerateToken(mapping.AccountId, mapping.Id, 5*time.Minute)
if err != nil {
log.Warnf("Failed to generate token for proxy %s mapping %s: %v", proxyID, mapping.Id, err)
continue
}
msg := shallowCloneMapping(mapping)
msg.AuthToken = token
resp = append(resp, msg)
}
if len(resp) == 0 {
return nil
}
return &proto.GetMappingUpdateResponse{
Mapping: resp,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/shared/grpc/proxy.go` around lines 457 - 479, In
perProxyMessage, avoid aborting the whole batch on a single token generation
error: when tokenStore.GenerateToken(mapping.AccountId, mapping.Id,
5*time.Minute) fails, log the error and skip that mapping (do not return nil),
continue processing the rest, and only return nil if the resulting resp slice is
empty; keep using shallowCloneMapping to copy entries and set msg.AuthToken for
successful tokens so SendServiceUpdate/SendServiceUpdateToCluster still receives
partial batches rather than discarding all mappings.

Comment on lines +717 to +720
err = am.reverseProxyManager.DeleteAllServices(ctx, accountID, userID)
if err != nil {
return status.Errorf(status.Internal, "failed to delete service %s: %v", accountID, err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against a nil reverseProxyManager before calling DeleteAllServices.

If reverseProxyManager isn’t configured (e.g., module disabled or not initialized in tests), this will panic during account deletion. Also, the error message reads like a single service deletion but uses the account ID.

🔧 Suggested fix
-	err = am.reverseProxyManager.DeleteAllServices(ctx, accountID, userID)
-	if err != nil {
-		return status.Errorf(status.Internal, "failed to delete service %s: %v", accountID, err)
-	}
+	if am.reverseProxyManager != nil {
+		err = am.reverseProxyManager.DeleteAllServices(ctx, accountID, userID)
+		if err != nil {
+			return status.Errorf(status.Internal, "failed to delete reverse proxy services for account %s: %v", accountID, err)
+		}
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err = am.reverseProxyManager.DeleteAllServices(ctx, accountID, userID)
if err != nil {
return status.Errorf(status.Internal, "failed to delete service %s: %v", accountID, err)
}
if am.reverseProxyManager != nil {
err = am.reverseProxyManager.DeleteAllServices(ctx, accountID, userID)
if err != nil {
return status.Errorf(status.Internal, "failed to delete reverse proxy services for account %s: %v", accountID, err)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/account.go` around lines 717 - 720, Guard against a nil
reverseProxyManager before calling DeleteAllServices: update the account
deletion flow (where am.reverseProxyManager.DeleteAllServices(ctx, accountID,
userID) is called) to first check if am.reverseProxyManager != nil and only call
DeleteAllServices when configured; if nil, skip the call. Also fix the error
message returned from status.Errorf to accurately describe the operation (e.g.,
"failed to delete reverse proxy services for account %s: %v") and keep using
accountID and err for context.

Comment on lines +1097 to +1110
// GetServicesByAccountID mocks base method.
func (m *MockStore) GetServicesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*reverseproxy.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetServicesByAccountID", ctx, lockStrength, accountID)
ret0, _ := ret[0].([]*reverseproxy.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}

// GetServicesByAccountID indicates an expected call of GetServicesByAccountID.
func (mr *MockStoreMockRecorder) GetServicesByAccountID(ctx, lockStrength, accountID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServicesByAccountID", reflect.TypeOf((*MockStore)(nil).GetServicesByAccountID), ctx, lockStrength, accountID)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "func.*GetServicesByAccountID|func.*GetAccountServices" --type=go -A 10 management/server/store/

Repository: netbirdio/netbird

Length of output: 5484


🏁 Script executed:

rg -n "\.GetServicesByAccountID|\.GetAccountServices" --type=go -B 2 -A 2

Repository: netbirdio/netbird

Length of output: 7321


🏁 Script executed:

sed -n '4909,4930p' management/server/store/sql_store.go

Repository: netbirdio/netbird

Length of output: 860


🏁 Script executed:

sed -n '4972,4993p' management/server/store/sql_store.go

Repository: netbirdio/netbird

Length of output: 856


Remove duplicate method GetServicesByAccountID — it is functionally identical to GetAccountServices.

Both GetServicesByAccountID and GetAccountServices have identical implementations in sql_store.go (lines 4909 and 4972). They perform the same database query, locking, decryption, and error handling. The only difference is naming convention and usage context (read vs. transaction), not actual behavior. Having two identically-implemented methods with different names in the Store interface unnecessarily duplicates the API surface and creates maintenance burden. Consolidate to a single method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/store/store_mock.go` around lines 1097 - 1110, Duplicate
API methods GetServicesByAccountID and GetAccountServices exist; remove the
redundant GetServicesByAccountID implementation and its mock helpers to
consolidate on a single method name (choose one, e.g., GetAccountServices).
Update the Store interface to only declare the chosen method, delete
MockStore.GetServicesByAccountID and
MockStoreMockRecorder.GetServicesByAccountID from store_mock.go, and update any
callers/tests to call the remaining method (GetAccountServices) and regenerate
mocks so gomock reflect types remain consistent.

Copy link
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.

🧹 Nitpick comments (1)
management/internals/modules/reverseproxy/interface_mock.go (1)

52-64: LGTM — new mock follows established patterns correctly.

The DeleteAllServices mock and its recorder are structurally identical to the other single-error-return mocked methods (e.g., DeleteService, ReloadService). Argument count in ctrl.Call (3) and RecordCallWithMethodType (3 variadic) matches the interface signature, and the reflect.TypeOf((*MockManager)(nil).DeleteAllServices) type-capture idiom is standard gomock v1.6.0 usage.

One optional process note: since this file carries // Code generated by MockGen. DO NOT EDIT., future interface changes should ideally be followed by regenerating the mock via go generate (or the equivalent mockgen invocation) to keep it in sync and avoid manual-edit drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/modules/reverseproxy/interface_mock.go` around lines 52
- 64, No changes required to the DeleteAllServices mock implementation
itself—DeleteAllServices on MockManager and its recorder MockManagerMockRecorder
correctly follow the gomock pattern—but ensure future interface updates are
propagated by regenerating the mocks (run mockgen or go generate for the
MockGen-generated file) so DeleteAllServices, MockManager and
MockManagerMockRecorder stay in sync with the real interface and avoid
manual-edit drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@management/internals/modules/reverseproxy/interface_mock.go`:
- Around line 52-64: No changes required to the DeleteAllServices mock
implementation itself—DeleteAllServices on MockManager and its recorder
MockManagerMockRecorder correctly follow the gomock pattern—but ensure future
interface updates are propagated by regenerating the mocks (run mockgen or go
generate for the MockGen-generated file) so DeleteAllServices, MockManager and
MockManagerMockRecorder stay in sync with the real interface and avoid
manual-edit drift.

Copy link
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.

🧹 Nitpick comments (1)
management/internals/shared/grpc/proxy_group_access_test.go (1)

20-22: No test cases exercise the DeleteAllServices path.

The mock satisfies the interface, but neither TestValidateUserGroupAccess nor TestGetAccountProxyByDomain invokes — or exercises an error path through — DeleteAllServices. If the account.go deletion flow is the primary consumer of this new method, consider adding a dedicated test (e.g., in proxy_test.go or a new table-driven case here) that verifies the bulk-delete call propagates correctly, including the error path via m.err.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/shared/grpc/proxy_group_access_test.go` around lines 20
- 22, Add a test that exercises mockReverseProxyManager.DeleteAllServices (both
success and error paths) so the account deletion flow calls and propagates its
result; update or add a table-driven case in proxy_group_access_test.go (or add
a new proxy_test.go) that triggers the account.go deletion logic and asserts
DeleteAllServices is invoked and that when mockReverseProxyManager.m.err is set
the error bubbles up; specifically reference the mock method DeleteAllServices
and the existing tests TestValidateUserGroupAccess and
TestGetAccountProxyByDomain as locations to mirror the pattern and assert proper
error propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@management/internals/shared/grpc/proxy_group_access_test.go`:
- Around line 20-22: Add a test that exercises
mockReverseProxyManager.DeleteAllServices (both success and error paths) so the
account deletion flow calls and propagates its result; update or add a
table-driven case in proxy_group_access_test.go (or add a new proxy_test.go)
that triggers the account.go deletion logic and asserts DeleteAllServices is
invoked and that when mockReverseProxyManager.m.err is set the error bubbles up;
specifically reference the mock method DeleteAllServices and the existing tests
TestValidateUserGroupAccess and TestGetAccountProxyByDomain as locations to
mirror the pattern and assert proper error propagation.

Copy link
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@management/server/account_test.go`:
- Around line 3126-3127: The test creates a ProxyServiceServer via
nbgrpc.NewProxyServiceServer and never stops its background goroutine; call
proxyGrpcServer.Close() during test teardown (or t.Cleanup) after creating
proxyGrpcServer and before calling
manager.SetServiceManager(reverseproxymanager.NewManager(...)) to avoid leaks;
additionally replace the nil accessLogManager/tokenStore/usersManager args with
lightweight no-op fakes (implementing SendAccessLog, ValidateSession,
ValidateUserGroupAccess) or safe mocks so future calls won't panic.

Comment on lines +3126 to +3127
proxyGrpcServer := nbgrpc.NewProxyServiceServer(nil, nil, nbgrpc.ProxyOIDCConfig{}, peersManager, nil)
manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, proxyGrpcServer, nil))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect ProxyServiceServer API and nil-usage patterns
rg -n --context 3 "type ProxyServiceServer" management/internals/shared/grpc/proxy.go
rg -n --context 3 "func \\(.*\\*ProxyServiceServer\\)" management/internals/shared/grpc/proxy.go
rg -n --context 3 "accessLogManager|tokenStore|usersManager" management/internals/shared/grpc/proxy.go
rg -n --context 3 "NewProxyServiceServer" -g'*.go'

Repository: netbirdio/netbird

Length of output: 19256


🏁 Script executed:

#!/bin/bash
# Find createManager function and its test usage
rg -n "func createManager|createManager\(" management/server/account_test.go | head -20
rg -n "defer.*Close|proxyGrpcServer.Close" management/server/account_test.go
# Check if any of the nil-sensitive methods are called in tests
rg -n "SendAccessLog|ValidateUserGroupAccess|ValidateSession|SendStatusUpdate" management/server/account_test.go
# Check delete account flows
rg -n "delete.*account|DeleteAccount" management/server/account_test.go | head -20

Repository: netbirdio/netbird

Length of output: 1485


🏁 Script executed:

#!/bin/bash
# Check DeleteAccount implementation
rg -n "func.*DeleteAccount" management/server/account.go
# Look for the actual implementation
sed -n '856,920p' management/server/account_test.go
# Check if reverse proxy operations are involved in delete
rg -n "reverseProxyManager|ServiceManager" management/server/account.go | head -30

Repository: netbirdio/netbird

Length of output: 2280


🏁 Script executed:

#!/bin/bash
# Check DeleteAllServices implementation in reverse proxy manager
rg -n "func.*DeleteAllServices" -A 30
# Also check if it accesses the proxy service server methods
rg -n "tokenStore|accessLogManager|usersManager" management/internals/business/reverseproxy/manager.go | head -20

Repository: netbirdio/netbird

Length of output: 13107


🏁 Script executed:

#!/bin/bash
# Get the complete DeleteAllServices implementation
sed -n '422,480p' management/internals/modules/reverseproxy/manager/manager.go

Repository: netbirdio/netbird

Length of output: 2150


Add cleanup for ProxyServiceServer background goroutine.

NewProxyServiceServer starts a background cleanup goroutine that is never stopped. The Close() method exists and should be called during test teardown. Since createManager is used in 20+ tests without cleanup, this causes goroutine leaks.

The nil parameters (accessLogManager, tokenStore, usersManager) passed in the test are a latent risk—they would panic if methods like SendAccessLog, ValidateSession, or ValidateUserGroupAccess are invoked, though the current delete-account flow does not directly trigger them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/account_test.go` around lines 3126 - 3127, The test creates
a ProxyServiceServer via nbgrpc.NewProxyServiceServer and never stops its
background goroutine; call proxyGrpcServer.Close() during test teardown (or
t.Cleanup) after creating proxyGrpcServer and before calling
manager.SetServiceManager(reverseproxymanager.NewManager(...)) to avoid leaks;
additionally replace the nil accessLogManager/tokenStore/usersManager args with
lightweight no-op fakes (implementing SendAccessLog, ValidateSession,
ValidateUserGroupAccess) or safe mocks so future calls won't panic.

@sonarqubecloud
Copy link

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.

1 participant

Comments