Skip to content

[management] Move permissions management from managers to API handlers#5482

Closed
pascal-fischer wants to merge 37 commits intomainfrom
refactor/permissions-manager
Closed

[management] Move permissions management from managers to API handlers#5482
pascal-fischer wants to merge 37 commits intomainfrom
refactor/permissions-manager

Conversation

@pascal-fischer
Copy link
Copy Markdown
Collaborator

@pascal-fischer pascal-fischer commented Mar 2, 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

  • Refactor

    • Permission checks centralized into HTTP middleware; backend modules no longer perform inline permission gating and manager wiring simplified.
  • Behavior

    • Handlers now receive resolved user authentication consistently, yielding uniform access control behavior across endpoints.
  • Tests

    • Added integration tests and test fixtures; unit/integration tests updated to exercise the centralized permission wrapper and expanded account scenarios.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 2, 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

Shifted permission enforcement to an HTTP middleware layer: added permission wrapper helpers, removed permissions.Manager from many managers and inline ValidateUserPermissions calls, updated handlers to accept explicit userAuth, and adjusted tests, mocks, interfaces, and SQL fixtures to the new wiring.

Changes

Cohort / File(s) Summary
Permissions core
management/internals/modules/permissions/manager.go, management/internals/modules/permissions/manager_mock.go
Add WithPermission, WrapHandler, AuthErrorHandler and mock support to enable request-time permission wrapping for HTTP handlers.
Manager removals (internals)
management/internals/modules/.../peers/manager.go, .../accesslogs/manager/manager.go, .../reverseproxy/.../manager.go, .../zones/.../manager.go, .../zones/records/.../manager.go
Remove permissions.Manager fields/params and delete inline ValidateUserPermissions usage; constructors updated to drop permissions dependency.
Manager removals (server)
management/server/* (account.go, group.go, peer.go, networks/, zones/, records/*, posture_checks.go, policy.go, route.go, nameserver.go, identity_provider.go, setupkey.go, settings/manager.go, etc.)
Eliminate in-method permission checks across many account- and resource-related managers; adjust signatures where required (e.g., GetPeers(..., all bool), GetGroupByName drops userID).
HTTP handlers & routing
management/server/http/handlers/..., management/server/http/handler.go, management/internals/modules/reverseproxy/.../api.go
Register endpoints via permissionsManager.WithPermission or permissions.WrapHandler; change handlers to accept userAuth *auth.UserAuth and remove in-handler context auth extraction.
Interfaces & mocks
management/server/account/manager.go, management/server/account/manager_mock.go, management/internals/modules/permissions/manager_mock.go, management/server/mock_server/account_mock.go
Update interfaces and mocks: add GetPeers(..., all bool), drop userID from GetGroupByName, and add mock WithPermission support.
Module wiring
management/internals/server/modules.go, management/internals/server/boot.go
Stop passing server PermissionsManager() into many module constructors; pass permissions into HTTP registration only; update module constructors to the new signatures.
Tests & test infra
many *_test.go, client/*, shared/management/client/client_test.go, management/server/http/testing/*, management/server/http/testing/testdata/*, management/server/http/testing/testing_tools/*
Switch tests to internals permissions package, update mocks/signatures, remove permission-denied test cases where applicable, add integration tests and SQL fixtures, and fix SQL timestamp formatting.
Deps & minor fixes
go.mod, management/internals/modules/reverseproxy/proxy/proxy.go, various SQL fixtures
Bump management-integrations/integrations pseudo-version; small struct tag formatting and timestamp/testdata fixes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PermMW as PermissionMiddleware
    participant Handler
    participant Manager
    participant Store

    Client->>PermMW: HTTP request
    PermMW->>PermMW: extract UserAuth & ValidateUserPermissions
    alt denied
        PermMW-->>Client: 403 / permission error
    else allowed
        PermMW->>Handler: call handler with userAuth
        Handler->>Manager: call manager (no inline permission checks)
        Manager->>Store: query or mutate
        Store-->>Manager: result
        Manager-->>Handler: result
        Handler-->>Client: HTTP response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • crn4

Poem

🐇
I hopped through routers, wrapped each gate,
Middleware now decides the fate.
Managers query stores unbound,
Handlers greet auth that’s passed around.
Thump-thump — tidy code, a lighter crate!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/permissions-manager

# Conflicts:
#	management/internals/modules/reverseproxy/service/manager/api.go
#	management/server/http/handler.go
…manager

# Conflicts:
#	management/internals/modules/reverseproxy/domain/manager/manager.go
#	management/internals/modules/reverseproxy/service/manager/api.go
#	management/internals/server/modules.go
#	management/server/http/testing/testing_tools/channel/channel.go
# Conflicts:
#	management/internals/modules/reverseproxy/service/manager/api.go
#	management/server/http/testing/testing_tools/channel/channel.go
# Conflicts:
#	management/server/account_test.go
#	management/server/http/handlers/networks/routers_handler.go
# Conflicts:
#	go.mod
#	go.sum
#	management/server/account_test.go
#	management/server/http/testing/testing_tools/channel/channel.go
@pascal-fischer pascal-fischer marked this pull request as ready for review April 20, 2026 09:32
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: 13

Caution

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

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

97-104: ⚠️ Potential issue | 🟡 Minor

Remove the stale manager-level permission denial case.

SaveDNSSettings no longer denies regular users at the manager layer, and this test currently accepts that success because shouldFail is only checked inside the error branch. Move this coverage to the HTTP permission-wrapper tests, or make the failure assertion explicit and remove this case here.

Proposed fix for the direct manager test
-		{
-			name:   "Should Not Update Settings As Regular User",
-			userID: dnsRegularUserID,
-			inputSettings: &types.DNSSettings{
-				DisabledManagementGroups: []string{dnsGroup1ID},
-			},
-			shouldFail: true,
-		},
@@
 			err = am.SaveDNSSettings(context.Background(), account.Id, testCase.userID, testCase.inputSettings)
-			if err != nil {
-				if testCase.shouldFail {
-					return
-				}
-				t.Error(err)
+			if testCase.shouldFail {
+				require.Error(t, err)
+				return
 			}
+			require.NoError(t, err)
 
 			updatedAccount, err := am.Store.GetAccount(context.Background(), account.Id)

Also applies to: 132-146

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

In `@management/server/dns_test.go` around lines 97 - 104, The test case "Should
Not Update Settings As Regular User" is stale because SaveDNSSettings no longer
denies regular users at the manager layer; remove or update this case in
management/server/dns_test.go (and the duplicate at the other location) by
either deleting the entry or changing it to assert success (set shouldFail=false
and assert no error / validate settings were saved), and move any HTTP
permission-wrapper-specific denial coverage to the HTTP/permission wrapper
tests; reference the SaveDNSSettings call and the test case name to locate and
update the code.
management/server/http/handlers/peers/peers_handler_test.go (1)

306-330: ⚠️ Potential issue | 🟡 Minor

Assert the recorder that actually served the request.

rr is never passed to router.ServeHTTP, so this status check can miss 401/403/500 responses from the wrapped handlers. Use res.StatusCode or recorder.Code instead.

Proposed fix
-	rr := httptest.NewRecorder()
-
 	p := initTestMetaData(t, peer, peer1)
@@
-			if status := rr.Code; status != tc.expectedStatus {
+			if status := res.StatusCode; status != tc.expectedStatus {
 				t.Fatalf("handler returned wrong status code: got %v want %v",
-					status, http.StatusOK)
+					status, tc.expectedStatus)
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/handlers/peers/peers_handler_test.go` around lines 306
- 330, The test is asserting the wrong recorder variable (rr) which was never
used with router.ServeHTTP; update the status assertion in Test (around the
request loop using initTestMetaData, router.HandleFunc and router.ServeHTTP) to
check the actual response from the served request — use recorder.Code (or
res.StatusCode) instead of rr.Code so that GetAllPeers/GetPeer/UpdatePeer
handler responses (401/403/500 etc.) are correctly asserted.
management/server/http/handlers/users/users_handler_test.go (1)

581-705: ⚠️ Potential issue | 🟡 Minor

Keep the “without auth” case routed through the wrapper.

The direct call now always passes a *auth.UserAuth, so the “without auth” case no longer verifies missing-auth handling. Use wrapHandler(userHandler.getCurrentUser) with no auth in the request context for that case.

Suggested direction
- userAuth := &auth.UserAuth{
- 	UserId:    tc.requestAuth.UserId,
- 	AccountId: existingAccountID,
- }
- userHandler.getCurrentUser(rr, req, userAuth)
+ router := mux.NewRouter()
+ router.HandleFunc("/api/users/current", wrapHandler(userHandler.getCurrentUser)).Methods(http.MethodGet)
+ router.ServeHTTP(rr, req)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/handlers/users/users_handler_test.go` around lines 581
- 705, The "without auth" test currently bypasses the HTTP wrapper by calling
userHandler.getCurrentUser directly with a non-nil *auth.UserAuth, so it doesn't
exercise missing-auth behavior; update the test loop to route the no-auth case
through wrapHandler(userHandler.getCurrentUser) and call its ServeHTTP(rr, req)
with no user set in the request, while keeping the existing pattern for cases
with auth (set nbcontext.SetUserAuthInRequest and call
userHandler.getCurrentUser(rr, req, userAuth)). Ensure you only construct and
pass the *auth.UserAuth (userAuth variable) for cases where
tc.requestAuth.UserId != "" and do not pass any userAuth for the wrapped call.
management/internals/modules/reverseproxy/service/manager/manager.go (1)

774-790: ⚠️ Potential issue | 🟠 Major

Delete service targets during bulk deletion too.

DeleteService removes targets before deleting the service, but DeleteAllServices skips that cleanup. Unless every store backend cascades this relationship, bulk deletion can leave orphan targets or fail on FK constraints.

Proposed fix
 	for _, svc := range services {
+		if err = transaction.DeleteServiceTargets(ctx, accountID, svc.ID); err != nil {
+			return fmt.Errorf("failed to delete service targets: %w", err)
+		}
 		if err = transaction.DeleteService(ctx, accountID, svc.ID); err != nil {
 			return fmt.Errorf("failed to delete service: %w", err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/modules/reverseproxy/service/manager/manager.go` around
lines 774 - 790, DeleteAllServices currently deletes services without removing
associated targets, which can leave orphans or violate FKs; update
Manager.DeleteAllServices so that inside the transaction loop you first call the
store method that removes service targets (e.g., transaction.DeleteTargets(ctx,
accountID, svc.ID) or whatever the store exposes to delete targets for a
service) and then call transaction.DeleteService(ctx, accountID, svc.ID); ensure
both calls run within the same ExecuteInTransaction block and propagate errors
from the target-deletion call just like the DeleteService call.
management/server/account.go (1)

1326-1356: ⚠️ Potential issue | 🟠 Major

Account membership validation gap in IsChild path.

The comment at line 1355–1356 is inaccurate. GetUserByUserID validates only that a user exists globally—it does not validate that the user belongs to the returned accountID.

For the IsChild path (line 1351–1352), getAccountIDWithAuthorizationClaims returns userAuth.AccountId after only checking that the account exists via AccountExists. The function never verifies that the user is actually a member of that account. The returned (accountID, user.Id) pair could represent a user in a different account.

While ValidateAccountAccess in the permissions middleware does check user.AccountID == accountID later in the HTTP flow, this validation happens after GetAccountIDFromUserAuth returns and the pair is propagated. Please add an explicit account membership validation here, or document why this ordering is safe.

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

In `@management/server/account.go` around lines 1326 - 1356,
GetAccountIDFromUserAuth currently returns (accountID, user.Id) for the
userAuth.IsChild path without ensuring the fetched user actually belongs to that
account; update GetAccountIDFromUserAuth to validate membership by comparing the
retrieved user's AccountID to the computed accountID (from
getAccountIDWithAuthorizationClaims) after calling Store.GetUserByUserID, and if
they differ return an appropriate error (e.g., permission denied or not found).
Ensure you reference GetAccountIDFromUserAuth,
getAccountIDWithAuthorizationClaims, and Store.GetUserByUserID when making this
check so the IsChild branch cannot return a user/account pair from different
accounts.
management/server/user.go (1)

906-939: ⚠️ Potential issue | 🔴 Critical

Nil pointer dereference when initiatorUserID == activity.SystemInitiator.

user is only populated inside the if initiatorUserID != activity.SystemInitiator branch (lines 909-915). When the initiator is the system, user stays nil, and line 922 user.HasAdminPower() || user.IsServiceUser will panic. The later case user != nil && user.AccountID == accountID guard at line 932 confirms nil is expected for this path, so the hasFullAccess computation must be guarded too.

Note the shadowed err inside the if block (line 910) is also dead since it never surfaces outside; collapse it.

🛡️ Proposed fix
 func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accountID, initiatorUserID string) (map[string]*types.UserInfo, error) {
 	var user *types.User
 	var err error
 	if initiatorUserID != activity.SystemInitiator {
-		result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID)
+		user, err = am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, initiatorUserID)
 		if err != nil {
 			return nil, fmt.Errorf("failed to get user: %w", err)
 		}
-		user = result
 	}
 
 	// Permission checks are now handled by the HTTP middleware via WithPermission wrapper
 	// This internal method is called from authenticated/authorized handlers
 	accountUsers := []*types.User{}
 
-	// Determine if user has full access based on their role
-	hasFullAccess := user.HasAdminPower() || user.IsServiceUser
+	// Determine if user has full access based on their role.
+	// System initiator has full access; otherwise, fall back to the user's role.
+	hasFullAccess := initiatorUserID == activity.SystemInitiator ||
+		(user != nil && (user.HasAdminPower() || user.IsServiceUser))
 
 	switch {
 	case hasFullAccess:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/user.go` around lines 906 - 939, In GetUsersFromAccount,
avoid the nil deref and err shadowing: collapse the shadowed err by assigning to
the outer err when calling am.Store.GetUserByUserID (use = not :=), and compute
hasFullAccess only if user is non-nil (e.g. hasFullAccess := user != nil &&
(user.HasAdminPower() || user.IsServiceUser)) before the switch so the
user.HasAdminPower/IsServiceUser calls cannot panic; leave the rest of the
switch logic and final call to BuildUserInfosForAccount unchanged.
management/server/http/handlers/policies/posture_checks_handler.go (1)

130-136: ⚠️ Potential issue | 🟠 Major

Guard req.Checks before dereferencing it.

A payload like {"name":"x"} leaves req.Checks nil and panics at req.Checks.GeoLocationCheck, turning a validation error into a 500/crash path.

🐛 Proposed fix
-	if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil {
+	if req.Checks == nil {
+		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "posture checks are required"), w)
+		return
+	}
+
+	if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil {
 		if p.geolocationManager == nil {
 			util.WriteError(r.Context(), status.Errorf(status.PreconditionFailed, "Geo location database is not initialized. "+
 				"Check the self-hosted Geo database documentation at https://docs.netbird.io/selfhosted/geo-support"), w)
 			return
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/handlers/policies/posture_checks_handler.go` around
lines 130 - 136, The code dereferences req.Checks without ensuring it's non-nil,
causing a panic for payloads that omit checks; update the handler to guard
req.Checks first (e.g., check req.Checks != nil) before accessing
req.Checks.GeoLocationCheck inside the posture checks logic in
posture_checks_handler.go (the block referencing req.Checks.GeoLocationCheck and
p.geolocationManager). Ensure the existing geolocationManager nil check and
error response remain unchanged but are executed only when req.Checks is non-nil
and GeoLocationCheck is present.
management/server/http/handlers/accounts/accounts_handler.go (1)

231-304: ⚠️ Potential issue | 🟠 Major

Bind {accountId} to the authenticated account before mutating.

WithPermission authorizes userAuth, but these handlers mutate the path accountId. Reject mismatches before validation/update/delete so a user authorized in one account cannot target another account ID.

🛡️ Proposed fix
 func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request, userAuth *auth.UserAuth) {
 	vars := mux.Vars(r)
 	accountID := vars["accountId"]
 	if len(accountID) == 0 {
 		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid accountID ID"), w)
 		return
 	}
+	if accountID != userAuth.AccountId {
+		util.WriteError(r.Context(), status.Errorf(status.PermissionDenied, "account ID does not match authenticated account"), w)
+		return
+	}
 
 	var req api.PutApiAccountsAccountIdJSONRequestBody
 func (h *handler) deleteAccount(w http.ResponseWriter, r *http.Request, userAuth *auth.UserAuth) {
 	vars := mux.Vars(r)
 	targetAccountID := vars["accountId"]
 	if len(targetAccountID) == 0 {
 		util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid account ID"), w)
 		return
 	}
+	if targetAccountID != userAuth.AccountId {
+		util.WriteError(r.Context(), status.Errorf(status.PermissionDenied, "account ID does not match authenticated account"), w)
+		return
+	}
 
 	err := h.accountManager.DeleteAccount(r.Context(), targetAccountID, userAuth.UserId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/handlers/accounts/accounts_handler.go` around lines
231 - 304, The handlers updateAccount and deleteAccount are currently mutating
whichever {accountId} is in the path without verifying it matches the caller's
authenticated account; add an early check in both updateAccount (after
extracting accountID) and deleteAccount (after extracting targetAccountID) that
compares the path value to the authenticated account identifier on userAuth
(e.g. userAuth.AccountId — if your auth struct uses a different field name
substitute that, e.g. userAuth.UserAccountId), and return an appropriate error
(status.PermissionDenied or status.InvalidArgument) if they differ so users
cannot mutate other accounts before calling
UpdateAccountSettings/DeleteAccount/UpdateAccountOnboarding/GetAccountMeta.
Ensure the check runs before any validation or calls to h.accountManager.
management/server/http/handlers/peers/peers_handler.go (1)

342-361: ⚠️ Potential issue | 🟠 Major

NetworkAdmin and other custom roles with Peers:Read permission are blocked by hard-coded role checks.

The middleware validates Peers:Read permission and allows the request, but the handler code at lines 343-361 performs its own authorization using only hard-coded checks for Admin/Owner/ServiceUser/IsChild. This means a NetworkAdmin (or any other role with explicit Peers:Read permission) receives an empty response when RegularUsersViewBlocked is enabled or when querying a non-owned peer, despite being authorized by the permission system.

The handler does not have access to the permission decision from the middleware. Either call ValidateUserPermissions within the handler to check the actual permission, or remove the hard-coded role checks and trust the middleware authorization.

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

In `@management/server/http/handlers/peers/peers_handler.go` around lines 342 -
361, The handler is re-checking authorization using hard-coded role checks
(user.Role, types.UserRoleAdmin, types.UserRoleOwner, user.IsServiceUser,
userAuth.IsChild) which blocks roles that were granted Peers:Read by middleware;
replace this local role-based gate with a real permission check: either call the
existing ValidateUserPermissions (or the middleware-exposed permission result)
inside the handler to verify Peers:Read for the current user, or remove the
hard-coded branch that uses account.Settings.RegularUsersViewBlocked / peer
ownership to rely on the middleware permission decision; ensure the code
references the same permission symbol (Peers:Read) and uses
ValidateUserPermissions (or the middleware flag) rather than
user.Role/User.IsServiceUser/userAuth.IsChild.
🧹 Nitpick comments (14)
management/internals/modules/zones/manager/manager_test.go (1)

29-52: Drop the now-unused gomock controller from the test helper.

After removing the permissions mock, ctrl is only created, returned, and finished. Removing it keeps the refactor tidy.

♻️ Proposed cleanup
-func setupTest(t *testing.T) (*managerImpl, store.Store, *mock_server.MockAccountManager, *gomock.Controller, func()) {
+func setupTest(t *testing.T) (*managerImpl, store.Store, *mock_server.MockAccountManager, func()) {
 	t.Helper()
@@
-	ctrl := gomock.NewController(t)
 	mockAccountManager := &mock_server.MockAccountManager{}
 
 	manager := NewManager(testStore, mockAccountManager, testDNSDomain).(*managerImpl)
 
-	return manager, testStore, mockAccountManager, ctrl, cleanup
+	return manager, testStore, mockAccountManager, cleanup
 }

Apply the same tuple cleanup at each setupTest(t) call site and remove the gomock import.

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

In `@management/internals/modules/zones/manager/manager_test.go` around lines 29 -
52, The setupTest helper currently creates and returns an unused gomock
controller (ctrl) — remove the gomock.Controller creation
(gomock.NewController(t)), drop ctrl from the setupTest return signature and
from the returned tuple, and delete any ctrl.Finish() calls in callers; also
remove the gomock import. Update all call sites of setupTest to stop expecting
the extra ctrl value (e.g., places that destructure manager, store,
mockAccountManager, ctrl, cleanup should now destructure manager, store,
mockAccountManager, cleanup). Ensure manager := NewManager(...) and mock_account
usage are unchanged.
management/server/http/handlers/routes/routes_handler_test.go (1)

28-38: Test helper looks good; consider sharing across handler tests.

wrapHandler duplicates the production middleware's auth-resolution behavior and is likely to be replicated verbatim in other handler test files updated in this PR (e.g., users, networks, etc.). Consider extracting it to a shared test utility (e.g., under management/server/http/testing or similar) to keep test wiring consistent as more handlers migrate to the userAuth-taking signature.

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

In `@management/server/http/handlers/routes/routes_handler_test.go` around lines
28 - 38, The test helper wrapHandler duplicates auth-resolution and should be
extracted to a shared test utility: create a new test-only package (e.g.,
management/server/http/testing) and move the function there, renaming it to
exported WrapHandler(func(w http.ResponseWriter, r *http.Request, userAuth
*auth.UserAuth)) http.HandlerFunc so other handler tests can reuse it; implement
it using nbcontext.GetUserAuthFromContext and the same error handling, update
all test files that used the local wrapHandler to import the new testing package
and call testing.WrapHandler, and adjust imports/visibility accordingly.
management/server/nameserver_test.go (1)

794-806: Reuse the existing peersManager instead of constructing a second one for the ephemeral manager.

Line 795 creates peersManager, then line 806 constructs a separate peers.NewManager(store) inline for ephemeral_manager.NewEphemeralManager(...). If peers.Manager carries any state (caches, subscribers, in-flight updates), the ephemeral manager will operate against a different instance than the rest of the wiring, which can silently mask test failures or cause races in concurrent tests.

♻️ Proposed fix
-	networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store)), &config.Config{})
+	networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/nameserver_test.go` around lines 794 - 806, The test
constructs two peers.Manager instances; replace the inline
peers.NewManager(store) passed to ephemeral_manager.NewEphemeralManager with the
already-created peersManager so the ephemeral manager shares the same instance
(i.e., change the call that currently uses peers.NewManager(store) to use
peersManager) to avoid divergent state/caches/subscribers between managers.
management/server/http/handlers/instance/instance_handler_test.go (1)

303-311: Assert the exact permission tuple in the endpoint wiring tests.

Using gomock.Any() for module and operation means these tests would still pass if /instance/version were accidentally wired to the wrong permission. Since this PR moves authorization to endpoint wrappers, please assert the expected modules.* and operations.* values here.

Also applies to: 338-346

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

In `@management/server/http/handlers/instance/instance_handler_test.go` around
lines 303 - 311, The test currently stubs permissionsManager.WithPermission with
gomock.Any() for module and operation, which allows incorrect permission wiring
to slip through; modify the mock expectation in instance_handler_test.go so
WithPermission is expected with the exact modules.Instance and operations.View
(or the correct operation constant used by AddVersionEndpoint) instead of
gomock.Any(), e.g., set
permissionsManager.EXPECT().WithPermission(modules.Instance, operations.View,
gomock.Any(), ...).DoAndReturn(...) (and make the same change in the other block
at lines 338-346) so the test asserts the exact permission tuple used by
AddVersionEndpoint.
management/server/dns.go (1)

21-27: Update the stale authorization comments.

These methods no longer validate user roles; authorization now belongs at the API handler layer.

Proposed fix
-// GetDNSSettings validates a user role and returns the DNS settings for the provided account ID
+// GetDNSSettings returns the DNS settings for the provided account ID.
 func (am *DefaultAccountManager) GetDNSSettings(ctx context.Context, accountID string, userID string) (*types.DNSSettings, error) {
 	return am.Store.GetAccountDNSSettings(ctx, store.LockingStrengthNone, accountID)
 }
 
-// SaveDNSSettings validates a user role and updates the account's DNS settings
+// SaveDNSSettings updates the account's DNS settings.
 func (am *DefaultAccountManager) SaveDNSSettings(ctx context.Context, accountID string, userID string, dnsSettingsToSave *types.DNSSettings) error {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/dns.go` around lines 21 - 27, The comments above
DefaultAccountManager.GetDNSSettings and DefaultAccountManager.SaveDNSSettings
are stale because these methods no longer perform user role validation; update
or remove the misleading “validates a user role” phrasing to reflect that
authorization is handled at the API handler layer — locate the comments on the
GetDNSSettings and SaveDNSSettings methods in management/server/dns.go and
change them to state that these methods fetch/update DNS settings without
performing authorization, delegating auth to the API layer.
management/server/http/handler.go (1)

156-177: Parameter ordering for permissionsManager / router is inconsistent across registration calls.

Most AddEndpoints variants take (..., router, permissionsManager) (e.g. lines 156, 158–168, 172, 174), but networks.AddEndpoints (line 169) and reverseproxymanager.RegisterEndpoints (line 176) take (..., permissionsManager, router), while the zones/records RegisterEndpoints put router first. Consider aligning all signatures to one convention to reduce the risk of mis-wiring when arguments are of compatible types. Since router and permissionsManager are distinct types the compiler will catch mistakes, so this is purely a readability nit.

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

In `@management/server/http/handler.go` around lines 156 - 177, Several endpoint
registration calls use inconsistent parameter ordering for router and
permissionsManager which harms readability; standardize all
AddEndpoints/RegisterEndpoints signatures and call sites to use the same
ordering (choose either ... , router, permissionsManager or ... ,
permissionsManager, router) and update the implementations and all call sites
accordingly—specifically update networks.AddEndpoints,
reverseproxymanager.RegisterEndpoints, zonesManager.RegisterEndpoints,
recordsManager.RegisterEndpoints (and their function definitions) to match the
chosen convention used by accounts.AddEndpoints, peers.AddEndpoints,
users.AddEndpoints, policies.AddEndpoints, groups.AddEndpoints,
routes.AddEndpoints, dns.AddEndpoints, events.AddEndpoints, idp.AddEndpoints,
and instance.AddEndpoints so every registration uses the identical parameter
order.
management/internals/modules/reverseproxy/domain/manager/manager.go (1)

51-104: Unused userID parameter in GetDomains after permissions removal.

With the permission check removed, userID is no longer referenced anywhere in GetDomains's body. CreateDomain/DeleteDomain/ValidateDomain still use it for StoreEvent, so this one stands out. You may want to either drop it for clarity or keep it for signature symmetry with the other methods — up to you.

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

In `@management/internals/modules/reverseproxy/domain/manager/manager.go` around
lines 51 - 104, GetDomains has an unused userID parameter; remove userID from
Manager.GetDomains signature and all call sites (and update the Manager
interface if present) so the method becomes GetDomains(ctx context.Context,
accountID string) ([]*domain.Domain, error); ensure any tests and usages are
updated accordingly (do not change CreateDomain/DeleteDomain/ValidateDomain
which still take userID), or if you prefer signature symmetry instead keep
userID but actually pass it into a StoreEvent or similar — choose one approach
and make the corresponding consistent changes across Manager, its interface, and
callers.
management/internals/modules/zones/manager/manager.go (1)

28-34: Document that these read methods no longer perform authorization.

userID is now unused in GetAllZones and GetZone, and no permission check is performed. Since the method signatures still take userID, a future caller could reasonably assume the manager is gating access. A short doc comment stating that authorization is the caller's responsibility (enforced by HTTP middleware) will prevent accidental misuse from non-HTTP code paths.

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

In `@management/internals/modules/zones/manager/manager.go` around lines 28 - 34,
The GetAllZones and GetZone methods on managerImpl currently accept a userID
parameter but perform no authorization; update their doc comments to state
clearly that these are read-only store accessors that do not enforce
permissions, that the userID parameter is unused and retained for signature
compatibility, and that authorization must be enforced by callers (e.g., HTTP
middleware); mention the exact methods GetAllZones and GetZone and advise
keeping the parameter for compatibility while documenting the responsibility
shift to callers to avoid accidental misuse from non-HTTP code paths.
management/server/group.go (1)

36-51: Redundant nil check after a successful GetUserByUserID.

Per the repository's Go convention, once err == nil you can rely on the returned value being populated; the if user == nil check is dead code unless the store contract explicitly documents otherwise. Collapse it.

♻️ Proposed fix
 	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
 	if err != nil {
 		return err
 	}
 
-	if user == nil {
-		return status.NewUserNotFoundError(userID)
-	}
-
 	if user.AccountID != accountID {
 		return status.NewUserNotPartOfAccountError()
 	}

Based on learnings from netbirdio/netbird PR #5441: methods returning (T, error) should propagate errors and avoid redundant nil checks on the returned value after the error path.

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

In `@management/server/group.go` around lines 36 - 51, Remove the redundant nil
check for user after calling am.Store.GetUserByUserID: since GetUserByUserID
returns (user, error) and the code already returned on err != nil, delete the if
user == nil { return status.NewUserNotFoundError(userID) } block and let
subsequent checks (user.AccountID, user.IsBlocked()) operate on the non-nil
user; keep the existing error-returning calls like
status.NewUserNotPartOfAccountError() and status.NewUserBlockedError() intact.
management/server/http/testing/integration/invites_handler_integration_test.go (3)

114-114: Simplify the URL construction.

strings.Replace("/api/users/invites/{inviteId}", "{inviteId}", "someInviteId", 1) is more verbose than needed; "/api/users/invites/someInviteId" (or fmt.Sprintf) reads more clearly and avoids the placeholder indirection.

Also applies to: 145-145

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

In
`@management/server/http/testing/integration/invites_handler_integration_test.go`
at line 114, The test constructs the request URL using an unnecessary
strings.Replace; update the call to testing_tools.BuildRequest in
invites_handler_integration_test.go (the req := testing_tools.BuildRequest(...)
line) to use a simple literal "/api/users/invites/someInviteId" or
fmt.Sprintf("/api/users/invites/%s", "someInviteId") instead of strings.Replace;
apply the same simplification to the similar call at the other occurrence around
line 145.

22-154: Factor out the shared user matrix across the four invite tests.

All four tests (Test_Invites_List, Test_Invites_Create, Test_Invites_Delete, Test_Invites_Regenerate) use an identical 8-entry user table. Extract it into a package-level var or a helper returning the slice to reduce duplication and keep future additions (new roles, new edge tokens) consistent.

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

In
`@management/server/http/testing/integration/invites_handler_integration_test.go`
around lines 22 - 154, Extract the repeated 8-entry user table used in
Test_Invites_List, Test_Invites_Create, Test_Invites_Delete and
Test_Invites_Regenerate into a single shared symbol (e.g., a package-level var
inviteTestUsers or a helper func getInviteTestUsers()) and replace each local
users := []struct{...}{...} block with a reference to that shared symbol; ensure
the shared data uses the same testing_tools constants (testing_tools.TestUserId,
TestAdminId, TestOwnerId, TestServiceUserId, TestServiceAdminId, BlockedUserId,
OtherUserId, InvalidToken) and keep the same fields (name, userId,
expectResponse) so existing test bodies (BuildRequest calls and
testing_tools.ReadResponse) require no other changes.

17-20: Test only asserts permission denial, not positive invite outcomes.

Because the integration infrastructure doesn't wire an embedded IDP, authorized users can only ever reach the PreconditionFailed branch. That's fine for verifying the permission middleware negative path, but it means these tests won't catch regressions in the actual invite handler logic (validation, token generation, etc.). Consider adding a companion test run with embedded IDP enabled, or at least a unit-level test over the handler bodies.

Also applies to: 46-48

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

In
`@management/server/http/testing/integration/invites_handler_integration_test.go`
around lines 17 - 20, The tests in invites_handler_integration_test.go only
exercise the permission-denial path (PreconditionFailed) and miss validating the
invite handler behavior; add either (a) a companion integration test that
enables the embedded IDP so an authorized flow reaches the invite handler and
asserts positive outcomes (validation, token generation, success status), or (b)
unit tests directly targeting the invite handler logic (the handler function
under test in this file and its internal validation/token generation routines)
to cover request validation, token creation, and success responses; ensure the
new tests verify happy-path status codes and payloads in addition to the
existing permission-denial assertions.
management/server/http/handlers/policies/geolocations_handler.go (1)

24-44: Consider dropping permissionsManager from geolocationsHandler now that WithPermission owns authorization.

The permissionsManager field on geolocationsHandler (line 28) is no longer referenced by getAllCountries/getCitiesByCountry since permission validation moved to the route wrapper. The field and its constructor plumbing (line 38-43) can be removed to reduce surface area.

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

In `@management/server/http/handlers/policies/geolocations_handler.go` around
lines 24 - 44, Remove the unused permissionsManager field from the
geolocationsHandler struct and stop passing it into the
constructor/newGeolocationsHandlerHandler; update newGeolocationsHandlerHandler
to only accept account.Manager and geolocation.Geolocation and return a
geolocationsHandler without the permissionsManager field, and update
AddLocationsEndpoints to construct the handler with only accountManager and
locationManager (but continue to use permissionsManager.WithPermission for the
routes). Also remove any references to the permissionsManager field inside
geolocationsHandler so the type and its constructor no longer carry the unused
dependency.
management/internals/modules/permissions/manager.go (1)

24-91: Document the dual-purpose AuthErrorHandler contract.

WithPermission invokes onAuthErr on two distinct failure modes:

  1. Validation failure from ValidateUserPermissions (e.g., user not found, blocked — returns wrapped error).
  2. Permission denied (status.NewPermissionDeniedError()).

Callers must inspect the error via status.FromError to distinguish these (the existing getOwnUser handler already relies on this). A short godoc on AuthErrorHandler and/or WithPermission noting that implementations receive different error types for these two cases, and that returning true suppresses the default util.WriteError, would prevent misuse.

Also: the variadic authErrHandler ...AuthErrorHandler silently ignores entries beyond the first. Either document this or tighten the signature to a single optional function pointer.

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

In `@management/internals/modules/permissions/manager.go` around lines 24 - 91,
The AuthErrorHandler contract is ambiguous: WithPermission calls the provided
onAuthErr for both validation failures from ValidateUserPermissions and for a
permission-denied case created with status.NewPermissionDeniedError(), and
returning true suppresses the default util.WriteError; add a short godoc to
AuthErrorHandler (and/or WithPermission) documenting the dual-purpose error
types (caller should use status.FromError to distinguish validation vs denied
errors) and that returning true prevents the default util.WriteError response,
and either change the variadic authErrHandler ...AuthErrorHandler to a single
optional AuthErrorHandler parameter or document that only the first element is
used (onAuthErr) to avoid silent ignores of additional handlers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ccc03b1e-5c15-4000-9b54-929c7ab1dab6

📥 Commits

Reviewing files that changed from the base of the PR and between 53b04e5 and a53c38a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (115)
  • client/cmd/testutil_test.go
  • client/internal/engine_test.go
  • client/server/server_test.go
  • go.mod
  • management/internals/modules/peers/manager.go
  • management/internals/modules/permissions/manager.go
  • management/internals/modules/permissions/manager_mock.go
  • management/internals/modules/permissions/modules/module.go
  • management/internals/modules/permissions/operations/operation.go
  • management/internals/modules/permissions/roles/admin.go
  • management/internals/modules/permissions/roles/auditor.go
  • management/internals/modules/permissions/roles/network_admin.go
  • management/internals/modules/permissions/roles/owner.go
  • management/internals/modules/permissions/roles/role_permissions.go
  • management/internals/modules/permissions/roles/user.go
  • management/internals/modules/reverseproxy/accesslogs/manager/api.go
  • management/internals/modules/reverseproxy/accesslogs/manager/manager.go
  • management/internals/modules/reverseproxy/domain/manager/api.go
  • management/internals/modules/reverseproxy/domain/manager/manager.go
  • management/internals/modules/reverseproxy/proxy/proxy.go
  • management/internals/modules/reverseproxy/service/manager/api.go
  • management/internals/modules/reverseproxy/service/manager/l4_port_test.go
  • management/internals/modules/reverseproxy/service/manager/manager.go
  • management/internals/modules/reverseproxy/service/manager/manager_test.go
  • management/internals/modules/zones/manager/api.go
  • management/internals/modules/zones/manager/manager.go
  • management/internals/modules/zones/manager/manager_test.go
  • management/internals/modules/zones/records/manager/api.go
  • management/internals/modules/zones/records/manager/manager.go
  • management/internals/modules/zones/records/manager/manager_test.go
  • management/internals/server/boot.go
  • management/internals/server/modules.go
  • management/server/account.go
  • management/server/account/manager.go
  • management/server/account/manager_mock.go
  • management/server/account_test.go
  • management/server/dns.go
  • management/server/dns_test.go
  • management/server/event.go
  • management/server/group.go
  • management/server/group_test.go
  • management/server/groups/manager.go
  • management/server/http/handler.go
  • management/server/http/handlers/accounts/accounts_handler.go
  • management/server/http/handlers/accounts/accounts_handler_test.go
  • management/server/http/handlers/dns/dns_settings_handler.go
  • management/server/http/handlers/dns/dns_settings_handler_test.go
  • management/server/http/handlers/dns/nameservers_handler.go
  • management/server/http/handlers/dns/nameservers_handler_test.go
  • management/server/http/handlers/events/events_handler.go
  • management/server/http/handlers/events/events_handler_test.go
  • management/server/http/handlers/groups/groups_handler.go
  • management/server/http/handlers/groups/groups_handler_test.go
  • management/server/http/handlers/idp/idp_handler.go
  • management/server/http/handlers/idp/idp_handler_test.go
  • management/server/http/handlers/instance/instance_handler.go
  • management/server/http/handlers/instance/instance_handler_test.go
  • management/server/http/handlers/networks/handler.go
  • management/server/http/handlers/networks/resources_handler.go
  • management/server/http/handlers/networks/routers_handler.go
  • management/server/http/handlers/peers/peers_handler.go
  • management/server/http/handlers/peers/peers_handler_test.go
  • management/server/http/handlers/policies/geolocation_handler_test.go
  • management/server/http/handlers/policies/geolocations_handler.go
  • management/server/http/handlers/policies/policies_handler.go
  • management/server/http/handlers/policies/policies_handler_test.go
  • management/server/http/handlers/policies/posture_checks_handler.go
  • management/server/http/handlers/policies/posture_checks_handler_test.go
  • management/server/http/handlers/routes/routes_handler.go
  • management/server/http/handlers/routes/routes_handler_test.go
  • management/server/http/handlers/setup_keys/setupkeys_handler.go
  • management/server/http/handlers/setup_keys/setupkeys_handler_test.go
  • management/server/http/handlers/users/invites_handler.go
  • management/server/http/handlers/users/invites_handler_test.go
  • management/server/http/handlers/users/pat_handler.go
  • management/server/http/handlers/users/pat_handler_test.go
  • management/server/http/handlers/users/users_handler.go
  • management/server/http/handlers/users/users_handler_test.go
  • management/server/http/testing/integration/groups_handler_integration_test.go
  • management/server/http/testing/integration/invites_handler_integration_test.go
  • management/server/http/testing/integration/posture_checks_handler_integration_test.go
  • management/server/http/testing/integration/users_approve_reject_handler_integration_test.go
  • management/server/http/testing/testdata/posture_checks.sql
  • management/server/http/testing/testdata/users_approve_reject.sql
  • management/server/http/testing/testing_tools/channel/channel.go
  • management/server/http/testing/testing_tools/db_verify.go
  • management/server/identity_provider.go
  • management/server/identity_provider_test.go
  • management/server/instance/manager_test.go
  • management/server/management_proto_test.go
  • management/server/management_test.go
  • management/server/mock_server/account_mock.go
  • management/server/nameserver.go
  • management/server/nameserver_test.go
  • management/server/networks/manager.go
  • management/server/networks/manager_test.go
  • management/server/networks/resources/manager.go
  • management/server/networks/resources/manager_test.go
  • management/server/networks/routers/manager.go
  • management/server/networks/routers/manager_test.go
  • management/server/peer.go
  • management/server/peer_test.go
  • management/server/policy.go
  • management/server/posture_checks.go
  • management/server/posture_checks_test.go
  • management/server/route.go
  • management/server/route_test.go
  • management/server/settings/manager.go
  • management/server/setupkey.go
  • management/server/types/account_test.go
  • management/server/user.go
  • management/server/user_invite_test.go
  • management/server/user_test.go
  • management/server/users/user.go
  • shared/management/client/client_test.go
💤 Files with no reviewable changes (3)
  • management/server/event.go
  • management/server/http/testing/integration/groups_handler_integration_test.go
  • management/server/posture_checks_test.go

Comment thread management/internals/modules/reverseproxy/domain/manager/api.go Outdated
Comment thread management/internals/modules/reverseproxy/service/manager/api.go
Comment thread management/server/http/handlers/events/events_handler_test.go Outdated
Comment thread management/server/http/handlers/groups/groups_handler.go Outdated
Comment thread management/server/http/handlers/peers/peers_handler.go
Comment thread management/server/http/testing/testdata/posture_checks.sql
Comment thread management/server/http/testing/testdata/posture_checks.sql Outdated
Comment thread management/server/http/testing/testdata/users_approve_reject.sql Outdated
Comment thread management/server/policy.go
Comment thread management/server/posture_checks.go
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.

🧹 Nitpick comments (3)
management/internals/modules/permissions/manager.go (2)

79-87: Permission-denied log level is arguably too low.

Denied permission checks are logged at Tracef, which is typically disabled in production. Since this is the main signal that an authenticated user attempted an unauthorized action, consider Debugf at minimum (or Infof if you want an audit trail). This is the only place the denial is recorded before the error response is written — Tracef makes real incidents effectively invisible.

-			log.WithContext(r.Context()).Tracef("user %s on account %s is not allowed to %s in %s", userAuth.UserId, userAuth.AccountId, operation, module)
+			log.WithContext(r.Context()).Debugf("user %s on account %s is not allowed to %s in %s", userAuth.UserId, userAuth.AccountId, operation, module)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/modules/permissions/manager.go` around lines 79 - 87,
Change the permission-denied logging from Tracef to a higher level so denials
are visible in typical production logs: locate the block in manager.go where
userAuth, operation and module are used (inside the permission check that
constructs permErr and calls onAuthErr and util.WriteError) and replace
log.WithContext(...).Tracef(...) with log.WithContext(...).Debugf(...) (or Infof
if you want an auditable record) so unauthorized attempts are recorded at a
visible log level before WriteError is called.

185-197: WrapHandler silently bypasses permission checks — document the intended use.

WrapHandler only extracts UserAuth; it performs no authorization. It's easy to grab this helper by mistake (especially given its position next to WithPermission) and end up registering a route that authenticates but never authorizes. Consider tightening the doc comment to explicitly state it must only be used for endpoints whose authorization is enforced elsewhere (e.g., same-user self-serve routes, or routes guarded by upstream middleware), and/or renaming to something like WrapHandlerWithoutPermissions/InjectUserAuth to make misuse visually obvious at call sites.

📝 Suggested doc tweak
-// WrapHandler wraps a handler that expects UserAuth with context extraction.
-// Unlike WithPermission, it does not perform any permission checks.
+// WrapHandler injects *auth.UserAuth (from the request context) into a handler
+// without performing any permission checks. It must only be used for endpoints
+// whose authorization is enforced by other means (e.g., the handler itself
+// validates ownership, or an upstream middleware gates access). For any
+// role/module-gated endpoint use WithPermission instead.
 func WrapHandler(h func(w http.ResponseWriter, r *http.Request, userAuth *auth.UserAuth)) http.HandlerFunc {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/internals/modules/permissions/manager.go` around lines 185 - 197,
WrapHandler currently only extracts UserAuth and performs no authorization,
which can be mistaken for WithPermission; update the doc comment for WrapHandler
to explicitly state it does NOT perform permission checks and must only be used
when authorization is enforced elsewhere (e.g., same-user self-serve routes or
upstream middleware). Optionally rename the function to a clearer identifier
such as InjectUserAuth or WrapHandlerWithoutPermissions and update all call
sites to use the new name to make misuse obvious; ensure references to
WithPermission remain unchanged so reviewers can compare intended behavior.
management/server/http/handlers/peers/peers_handler_test.go (1)

16-18: Two mock libraries in the same test file — consider unifying.

This file imports both github.com/golang/mock/gomock (as gomock) and go.uber.org/mock/gomock (as ugomock), creating two separate controllers (ctrl via ugomock, ctrl2 via gomock) because network_map.NewMockController and permissions.NewMockManager were generated against different mock packages. github.com/golang/mock is archived; go.uber.org/mock is its active successor. The two APIs are source-compatible, but this pattern is fragile and confusing. Consider regenerating permissions.NewMockManager with go.uber.org/mock/mockgen (updating the //go:generate directive in management/internals/modules/permissions/manager.go) so the codebase uses one mock library and one controller per test. This would require updates to several test files that import permissions.NewMockManager.

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

In `@management/server/http/handlers/peers/peers_handler_test.go` around lines 16
- 18, This test imports two mock packages causing two controllers (ctrl via
go.uber.org/mock/gomock and ctrl2 via github.com/golang/mock/gomock) because
network_map.NewMockController and permissions.NewMockManager were generated with
different mock generators; regenerate permissions.NewMockManager using
go.uber.org/mock/mockgen (update the //go:generate in
management/internals/modules/permissions/manager.go) so both mocks come from
go.uber.org/mock, then update this test to import only go.uber.org/mock/gomock
(remove github.com/golang/mock/gomock), use a single controller (e.g., ctrl from
ugomock.NewController) to create both network_map.NewMockController and
permissions.NewMockManager, and adjust any other tests that reference
permissions.NewMockManager to the new mock package.
🤖 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/permissions/manager.go`:
- Around line 79-87: Change the permission-denied logging from Tracef to a
higher level so denials are visible in typical production logs: locate the block
in manager.go where userAuth, operation and module are used (inside the
permission check that constructs permErr and calls onAuthErr and
util.WriteError) and replace log.WithContext(...).Tracef(...) with
log.WithContext(...).Debugf(...) (or Infof if you want an auditable record) so
unauthorized attempts are recorded at a visible log level before WriteError is
called.
- Around line 185-197: WrapHandler currently only extracts UserAuth and performs
no authorization, which can be mistaken for WithPermission; update the doc
comment for WrapHandler to explicitly state it does NOT perform permission
checks and must only be used when authorization is enforced elsewhere (e.g.,
same-user self-serve routes or upstream middleware). Optionally rename the
function to a clearer identifier such as InjectUserAuth or
WrapHandlerWithoutPermissions and update all call sites to use the new name to
make misuse obvious; ensure references to WithPermission remain unchanged so
reviewers can compare intended behavior.

In `@management/server/http/handlers/peers/peers_handler_test.go`:
- Around line 16-18: This test imports two mock packages causing two controllers
(ctrl via go.uber.org/mock/gomock and ctrl2 via github.com/golang/mock/gomock)
because network_map.NewMockController and permissions.NewMockManager were
generated with different mock generators; regenerate permissions.NewMockManager
using go.uber.org/mock/mockgen (update the //go:generate in
management/internals/modules/permissions/manager.go) so both mocks come from
go.uber.org/mock, then update this test to import only go.uber.org/mock/gomock
(remove github.com/golang/mock/gomock), use a single controller (e.g., ctrl from
ugomock.NewController) to create both network_map.NewMockController and
permissions.NewMockManager, and adjust any other tests that reference
permissions.NewMockManager to the new mock package.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7bf8fde9-915f-46b7-a73f-8aff966d2f08

📥 Commits

Reviewing files that changed from the base of the PR and between a53c38a and b65a8bc.

📒 Files selected for processing (30)
  • management/internals/modules/permissions/manager.go
  • management/internals/modules/reverseproxy/domain/manager/api.go
  • management/server/http/handlers/accounts/accounts_handler_test.go
  • management/server/http/handlers/dns/dns_settings_handler_test.go
  • management/server/http/handlers/dns/nameservers_handler_test.go
  • management/server/http/handlers/events/events_handler_test.go
  • management/server/http/handlers/groups/groups_handler_test.go
  • management/server/http/handlers/idp/idp_handler_test.go
  • management/server/http/handlers/peers/peers_handler_test.go
  • management/server/http/handlers/policies/geolocation_handler_test.go
  • management/server/http/handlers/policies/policies_handler_test.go
  • management/server/http/handlers/policies/posture_checks_handler_test.go
  • management/server/http/handlers/routes/routes_handler_test.go
  • management/server/http/handlers/setup_keys/setupkeys_handler_test.go
  • management/server/http/handlers/users/pat_handler_test.go
  • management/server/http/handlers/users/users_handler_test.go
  • management/server/http/testing/testdata/accounts.sql
  • management/server/http/testing/testdata/dns.sql
  • management/server/http/testing/testdata/events.sql
  • management/server/http/testing/testdata/groups.sql
  • management/server/http/testing/testdata/networks.sql
  • management/server/http/testing/testdata/peers.sql
  • management/server/http/testing/testdata/peers_integration.sql
  • management/server/http/testing/testdata/policies.sql
  • management/server/http/testing/testdata/posture_checks.sql
  • management/server/http/testing/testdata/routes.sql
  • management/server/http/testing/testdata/setup_keys.sql
  • management/server/http/testing/testdata/users.sql
  • management/server/http/testing/testdata/users_approve_reject.sql
  • management/server/http/testing/testdata/users_integration.sql
✅ Files skipped from review due to trivial changes (2)
  • management/server/http/handlers/routes/routes_handler_test.go
  • management/server/http/handlers/policies/posture_checks_handler_test.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • management/server/http/handlers/events/events_handler_test.go
  • management/server/http/handlers/policies/policies_handler_test.go
  • management/server/http/handlers/idp/idp_handler_test.go
  • management/server/http/handlers/dns/nameservers_handler_test.go
  • management/server/http/handlers/setup_keys/setupkeys_handler_test.go
  • management/server/http/handlers/policies/geolocation_handler_test.go
  • management/server/http/handlers/dns/dns_settings_handler_test.go
  • management/server/http/handlers/accounts/accounts_handler_test.go
  • management/server/http/testing/testdata/posture_checks.sql
  • management/server/http/testing/testdata/users_approve_reject.sql
  • management/server/http/handlers/users/users_handler_test.go

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: 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/http/handlers/groups/groups_handler.go`:
- Around line 45-47: The current canReadPeers suppresses ValidateUserPermissions
errors by returning false; change its signature to return (bool, error) and
propagate the validation error instead of converting it to false (i.e., have
canReadPeers call h.permissionsManager.ValidateUserPermissions and return
(allowed, err)). Update all call sites (handlers that call canReadPeers before
calling GetPeers and the createGroup/updateGroup handlers) to handle the error:
call the new canReadPeers before invoking GetPeers (and for
createGroup/updateGroup call it before CreateGroup/UpdateGroup) and
return/handle the error rather than proceeding with a silently downgraded
permission result. Ensure references: canReadPeers, ValidateUserPermissions,
GetPeers, createGroup, updateGroup, and permissionsManager are updated
accordingly.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77d31e63-9bc9-4ef9-8d32-796c2e31a892

📥 Commits

Reviewing files that changed from the base of the PR and between b65a8bc and bed8d89.

📒 Files selected for processing (2)
  • management/server/http/handlers/groups/groups_handler.go
  • management/server/http/handlers/groups/groups_handler_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/server/http/handlers/groups/groups_handler_test.go

Comment thread management/server/http/handlers/groups/groups_handler.go
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.

♻️ Duplicate comments (1)
management/server/posture_checks.go (1)

20-31: ⚠️ Potential issue | 🔴 Critical

Bind create/update behavior to the authorized route operation.

create is still ignored, so the request body ID decides whether this executes the update path. With posture-check permissions now enforced by route middleware, a caller authorized for create can still update an existing posture check by sending postureChecks.ID.

🛡️ Proposed fix
 func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error) {
 	var updateAccountPeers bool
 	var err error
-	var isUpdate = postureChecks.ID != ""
+	if create && postureChecks.ID != "" {
+		return nil, status.Errorf(status.InvalidArgument, "posture check ID must be empty when creating a posture check")
+	}
+	if !create && postureChecks.ID == "" {
+		return nil, status.Errorf(status.InvalidArgument, "posture check ID is required when updating a posture check")
+	}
+
+	var isUpdate = !create
 	var action = activity.PostureCheckCreated
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/posture_checks.go` around lines 20 - 31, SavePostureChecks
currently derives update vs create from postureChecks.ID (isUpdate) and ignores
the create parameter; change the flow so the create boolean controls the
operation: inside SavePostureChecks use create to decide create vs update (e.g.,
treat isUpdate = !create), validate that when create==true postureChecks.ID must
be empty (return an error if not), and when create==false postureChecks.ID must
be present (return an error if missing); ensure the activity action variable
(currently activity.PostureCheckCreated) is set appropriately for updates vs
creates and that existing validation calls like validatePostureChecks are
executed in both paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@management/server/posture_checks.go`:
- Around line 20-31: SavePostureChecks currently derives update vs create from
postureChecks.ID (isUpdate) and ignores the create parameter; change the flow so
the create boolean controls the operation: inside SavePostureChecks use create
to decide create vs update (e.g., treat isUpdate = !create), validate that when
create==true postureChecks.ID must be empty (return an error if not), and when
create==false postureChecks.ID must be present (return an error if missing);
ensure the activity action variable (currently activity.PostureCheckCreated) is
set appropriately for updates vs creates and that existing validation calls like
validatePostureChecks are executed in both paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9190b258-62a2-489c-81d1-a72126229471

📥 Commits

Reviewing files that changed from the base of the PR and between bed8d89 and 0b04c0d.

📒 Files selected for processing (2)
  • management/server/policy.go
  • management/server/posture_checks.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/server/policy.go

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.

Caution

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

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

1343-1360: ⚠️ Potential issue | 🟠 Major

Validate the loaded user belongs to the resolved account.

After removing the manager-level permission check, the IsChild path can return userAuth.AccountId after only checking the account exists. GetUserByUserID proves the user exists, but not that user.AccountID == accountID; add that guard before the IsChild early return.

Suggested fix
 	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userAuth.UserId)
 	if err != nil {
 		// this is not really possible because we got an account by user ID
 		log.Errorf("failed to get user by ID %s: %v", userAuth.UserId, err)
 		return "", "", status.Errorf(status.NotFound, "user %s not found", userAuth.UserId)
 	}
 
+	if user.AccountID != accountID {
+		return "", "", status.Errorf(status.PermissionDenied, "user %s is not part of account %s", user.Id, accountID)
+	}
+
 	if userAuth.IsChild {
 		return accountID, user.Id, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/account.go` around lines 1343 - 1360, The IsChild
early-return path in the logic that calls getAccountIDWithAuthorizationClaims
and Store.GetUserByUserID must verify the loaded user actually belongs to the
resolved account; update the code around getAccountIDWithAuthorizationClaims,
Store.GetUserByUserID and the IsChild branch to check that user.AccountID (or
the field name used on the returned user) equals accountID and if not return an
appropriate NotFound or Permission error instead of returning accountID and
user.Id; ensure the check occurs before the early return and use the same
status.Errorf pattern already used for missing users.
management/server/user_test.go (1)

1915-2000: 🛠️ Refactor suggestion | 🟠 Major

HTTP handler is the only entrypoint; confirm integration tests cover permission rejection.

Verification confirms ApproveUser and RejectUser are called exclusively from management/server/http/handlers/users/users_handler.go. However, the manager implementations contain no permission checks—they only validate user existence and pending approval status. Permission gating is enforced by the HTTP middleware (permissionsManager.WithPermission).

The integration tests (TestApproveUserEndpoint, TestRejectUserEndpoint) only verify admin success cases and do not test permission rejection for non-admin callers. If a non-HTTP code path (gRPC, background job) calls the manager in the future, there is no permission guard.

Action: Either confirm this architecture is intentional and add an integration test case verifying non-admin rejection at the HTTP level, or add a manager-level unit test asserting non-admin users cannot approve/reject.

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

In `@management/server/user_test.go` around lines 1915 - 2000, The manager methods
ApproveUser and RejectUser lack permission checks; add a test to cover
permission rejection: either add an HTTP-level integration test (alongside
TestApproveUserEndpoint/TestRejectUserEndpoint) that issues the approve/reject
requests authenticated as a non-admin user and asserts a 403/permission-denied
response, or add unit tests that call manager.ApproveUser and manager.RejectUser
using a non-admin caller and assert they return a permission error; reference
the handler wrapper permissionsManager.WithPermission to ensure the test
verifies the enforcement point you choose.
♻️ Duplicate comments (1)
management/server/http/handlers/users/users_handler.go (1)

30-38: ⚠️ Potential issue | 🟠 Major

Self-service endpoints still need permission-denied fallbacks.

/users/current and /users/{userId}/password now require modules.Users permissions before reaching handlers. Regular users without those permissions can be blocked from viewing their own profile or changing their own password; mirror the /users fallback pattern for these self-service routes.

Suggested shape
-	router.HandleFunc("/users/current", permissionsManager.WithPermission(modules.Users, operations.Read, userHandler.getCurrentUser)).Methods("GET", "OPTIONS")
+	router.HandleFunc("/users/current", permissionsManager.WithPermission(modules.Users, operations.Read, userHandler.getCurrentUser, userHandler.getCurrentUserFallback)).Methods("GET", "OPTIONS")
...
-	router.HandleFunc("/users/{userId}/password", permissionsManager.WithPermission(modules.Users, operations.Update, userHandler.changePassword)).Methods("PUT", "OPTIONS")
+	router.HandleFunc("/users/{userId}/password", permissionsManager.WithPermission(modules.Users, operations.Update, userHandler.changePassword, userHandler.changeOwnPassword)).Methods("PUT", "OPTIONS")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/handlers/users/users_handler.go` around lines 30 - 38,
The /users/current and /users/{userId}/password routes currently require
modules.Users permission and lack the self-service fallback used by the /users
route; update the router registrations to use the same WithPermission
dual-handler pattern so that users without global Users permission can still
access their own data: for "/users/current" call
permissionsManager.WithPermission(modules.Users, operations.Read,
userHandler.getCurrentUser, userHandler.getOwnUser) and for
"/users/{userId}/password" call permissionsManager.WithPermission(modules.Users,
operations.Update, userHandler.changePassword, userHandler.changeOwnPassword)
(or implement changeOwnPassword if missing) so the second handler acts as the
self-service fallback.
🧹 Nitpick comments (4)
management/server/http/testing/testing_tools/tools.go (1)

108-114: Consider also rejecting other 2xx success codes in negative-path assertions.

The new guard only fails on 200 and 201, but a handler that reaches success via 202 Accepted or 204 No Content would still silently pass in the !expectResponse branch — which defeats the intent of catching "handler reached and succeeded" when permission middleware should have short-circuited the request. If negative tests never expect any 2xx, a broader check (recorder.Code >= 200 && recorder.Code < 300) would be more robust.

Proposed tightening
-		if recorder.Code == http.StatusOK || recorder.Code == http.StatusCreated {
-			t.Fatalf("expected unauthorized/error status code but got %d, content: %s",
-				recorder.Code, string(content))
-		}
+		if recorder.Code >= http.StatusOK && recorder.Code < http.StatusMultipleChoices {
+			t.Fatalf("expected unauthorized/error status code but got %d, content: %s",
+				recorder.Code, string(content))
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/testing/testing_tools/tools.go` around lines 108 -
114, The negative-path check in testing_tools (using expectResponse and
recorder.Code) only rejects 200 and 201; update the logic that currently checks
"recorder.Code == http.StatusOK || recorder.Code == http.StatusCreated" to
instead treat any 2xx as a success (i.e., check recorder.Code >= 200 &&
recorder.Code < 300) so tests that should be blocked by middleware also fail
when handlers return 202, 204, etc.; adjust the branch that returns nil, false
accordingly to use this broader 2xx guard.
management/server/http/handlers/accounts/accounts_handler.go (1)

295-309: Nit: Use accountID in DeleteAccount call for consistency.

After the mismatch check on line 297, accountID and userAuth.AccountId are equivalent, but updateAccount uses the local accountID variable for downstream calls while deleteAccount uses userAuth.AccountId. Aligning them makes the code consistent and slightly more defensive against future edits that might shift the mismatch check.

♻️ Proposed change
-	err := h.accountManager.DeleteAccount(r.Context(), userAuth.AccountId, userAuth.UserId)
+	err := h.accountManager.DeleteAccount(r.Context(), accountID, userAuth.UserId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/http/handlers/accounts/accounts_handler.go` around lines
295 - 309, In deleteAccount, use the local accountID variable when calling
h.accountManager.DeleteAccount instead of userAuth.AccountId to match the
mismatch check and the pattern used in updateAccount; update the call in the
deleteAccount function to pass accountID and keep userAuth.UserId as the second
arg so the call reads DeleteAccount(ctx, accountID, userAuth.UserId).
management/server/http/testing/integration/users_handler_integration_test.go (1)

640-670: Tighten cross-account assertions to a specific expected status.

assert.NotEqual(t, http.StatusOK, ...) also passes on a 500/panic-recovered response, which would hide a regression where the handler crashes instead of properly denying the request. Prefer asserting the concrete expected status (e.g., http.StatusForbidden / http.StatusNotFound) so the test fails loudly if the denial path changes to an internal error.

♻️ Example
-		assert.NotEqual(t, http.StatusOK, recorder.Code, "cross-account user update must be rejected")
+		assert.Equal(t, http.StatusForbidden, recorder.Code, "cross-account user update must be rejected with 403")

Same applies to Test_Users_Delete_CrossAccountAttack on line 668.

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

In `@management/server/http/testing/integration/users_handler_integration_test.go`
around lines 640 - 670, The tests Test_Users_Update_CrossAccountAttack and
Test_Users_Delete_CrossAccountAttack currently use assert.NotEqual(t,
http.StatusOK, recorder.Code) which masks 500/internal errors; change these to
assert.Equal with the concrete expected denial status (e.g., assert.Equal(t,
http.StatusForbidden, recorder.Code, "cross-account user update must be
rejected") and similarly for the delete test) so the test fails loudly if the
handler returns 500 instead of the intended forbidden/not-found response.
management/server/http/testing/integration/accounts_handler_integration_test.go (1)

259-299: Test_Accounts_Delete should verify DB side-effect; cross-account tests should assert specific status.

Two related test-quality issues:

  1. Test_Accounts_Delete only asserts the response code via ReadResponse and never checks that the owner case actually removed the account from the DB (unlike Test_Users_Delete which uses VerifyUserNotInDB). For a destructive operation, missing the DB verification means a silent regression (e.g., handler returning 200 without deleting) would not be caught.

  2. Test_Accounts_Delete_CrossAccountAttack (and Test_Accounts_Update_CrossAccountAttack at lines 236–257) use assert.NotEqual(t, http.StatusOK, ...), which also passes for 500/panic-recovered responses. Prefer asserting the concrete expected status (e.g., http.StatusForbidden) so a regression that turns the permission denial into an internal error is caught.

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

In
`@management/server/http/testing/integration/accounts_handler_integration_test.go`
around lines 259 - 299, Test_Accounts_Delete currently only checks HTTP response
but not DB side-effects and the cross-account tests use NotEqual which can hide
500s; update Test_Accounts_Delete so that when the "Owner user" case returns
success you also verify the account was removed from the DB (call the existing
helper pattern used in Test_Users_Delete — e.g.,
testing_tools.VerifyAccountNotInDB(t, db/conn, testing_tools.TestAccountId) or
add a similar VerifyAccountNotInDB helper if missing) and change
Test_Accounts_Delete_CrossAccountAttack (and
Test_Accounts_Update_CrossAccountAttack) to assert the concrete expected status
(e.g., assert.Equal(t, http.StatusForbidden, recorder.Code)) instead of
assert.NotEqual to ensure permission failures, not internal errors, are
enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@management/server/account.go`:
- Around line 1343-1360: The IsChild early-return path in the logic that calls
getAccountIDWithAuthorizationClaims and Store.GetUserByUserID must verify the
loaded user actually belongs to the resolved account; update the code around
getAccountIDWithAuthorizationClaims, Store.GetUserByUserID and the IsChild
branch to check that user.AccountID (or the field name used on the returned
user) equals accountID and if not return an appropriate NotFound or Permission
error instead of returning accountID and user.Id; ensure the check occurs before
the early return and use the same status.Errorf pattern already used for missing
users.

In `@management/server/user_test.go`:
- Around line 1915-2000: The manager methods ApproveUser and RejectUser lack
permission checks; add a test to cover permission rejection: either add an
HTTP-level integration test (alongside
TestApproveUserEndpoint/TestRejectUserEndpoint) that issues the approve/reject
requests authenticated as a non-admin user and asserts a 403/permission-denied
response, or add unit tests that call manager.ApproveUser and manager.RejectUser
using a non-admin caller and assert they return a permission error; reference
the handler wrapper permissionsManager.WithPermission to ensure the test
verifies the enforcement point you choose.

---

Duplicate comments:
In `@management/server/http/handlers/users/users_handler.go`:
- Around line 30-38: The /users/current and /users/{userId}/password routes
currently require modules.Users permission and lack the self-service fallback
used by the /users route; update the router registrations to use the same
WithPermission dual-handler pattern so that users without global Users
permission can still access their own data: for "/users/current" call
permissionsManager.WithPermission(modules.Users, operations.Read,
userHandler.getCurrentUser, userHandler.getOwnUser) and for
"/users/{userId}/password" call permissionsManager.WithPermission(modules.Users,
operations.Update, userHandler.changePassword, userHandler.changeOwnPassword)
(or implement changeOwnPassword if missing) so the second handler acts as the
self-service fallback.

---

Nitpick comments:
In `@management/server/http/handlers/accounts/accounts_handler.go`:
- Around line 295-309: In deleteAccount, use the local accountID variable when
calling h.accountManager.DeleteAccount instead of userAuth.AccountId to match
the mismatch check and the pattern used in updateAccount; update the call in the
deleteAccount function to pass accountID and keep userAuth.UserId as the second
arg so the call reads DeleteAccount(ctx, accountID, userAuth.UserId).

In
`@management/server/http/testing/integration/accounts_handler_integration_test.go`:
- Around line 259-299: Test_Accounts_Delete currently only checks HTTP response
but not DB side-effects and the cross-account tests use NotEqual which can hide
500s; update Test_Accounts_Delete so that when the "Owner user" case returns
success you also verify the account was removed from the DB (call the existing
helper pattern used in Test_Users_Delete — e.g.,
testing_tools.VerifyAccountNotInDB(t, db/conn, testing_tools.TestAccountId) or
add a similar VerifyAccountNotInDB helper if missing) and change
Test_Accounts_Delete_CrossAccountAttack (and
Test_Accounts_Update_CrossAccountAttack) to assert the concrete expected status
(e.g., assert.Equal(t, http.StatusForbidden, recorder.Code)) instead of
assert.NotEqual to ensure permission failures, not internal errors, are
enforced.

In
`@management/server/http/testing/integration/users_handler_integration_test.go`:
- Around line 640-670: The tests Test_Users_Update_CrossAccountAttack and
Test_Users_Delete_CrossAccountAttack currently use assert.NotEqual(t,
http.StatusOK, recorder.Code) which masks 500/internal errors; change these to
assert.Equal with the concrete expected denial status (e.g., assert.Equal(t,
http.StatusForbidden, recorder.Code, "cross-account user update must be
rejected") and similarly for the delete test) so the test fails loudly if the
handler returns 500 instead of the intended forbidden/not-found response.

In `@management/server/http/testing/testing_tools/tools.go`:
- Around line 108-114: The negative-path check in testing_tools (using
expectResponse and recorder.Code) only rejects 200 and 201; update the logic
that currently checks "recorder.Code == http.StatusOK || recorder.Code ==
http.StatusCreated" to instead treat any 2xx as a success (i.e., check
recorder.Code >= 200 && recorder.Code < 300) so tests that should be blocked by
middleware also fail when handlers return 202, 204, etc.; adjust the branch that
returns nil, false accordingly to use this broader 2xx guard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a34ccc88-36ed-4e22-be79-3a8f99ebf09c

📥 Commits

Reviewing files that changed from the base of the PR and between 0b04c0d and 4703070.

📒 Files selected for processing (13)
  • management/server/account.go
  • management/server/http/handlers/accounts/accounts_handler.go
  • management/server/http/handlers/users/users_handler.go
  • management/server/http/testing/integration/accounts_handler_integration_test.go
  • management/server/http/testing/integration/users_handler_integration_test.go
  • management/server/http/testing/testdata/users_integration.sql
  • management/server/http/testing/testing_tools/tools.go
  • management/server/store/sql_store.go
  • management/server/store/sql_store_user_invite_test.go
  • management/server/store/store.go
  • management/server/store/store_mock.go
  • management/server/user.go
  • management/server/user_test.go
✅ Files skipped from review due to trivial changes (1)
  • management/server/http/testing/testdata/users_integration.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/server/user.go

@sonarqubecloud
Copy link
Copy Markdown

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