Allow technicians to transfer hosts#45956
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #45956 +/- ##
==========================================
+ Coverage 66.78% 66.81% +0.03%
==========================================
Files 2751 2754 +3
Lines 219892 220120 +228
Branches 10880 11025 +145
==========================================
+ Hits 146846 147076 +230
+ Misses 59775 59760 -15
- Partials 13271 13284 +13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Pull request overview
Adds a dedicated “transfer host” authorization action so technicians can transfer hosts between fleets per the updated permissions model, and exposes the Transfer UI controls to global technicians on Fleet Premium.
Changes:
- Introduces
ActionTransferHostand corresponding OPA policy rules for global and team-scoped technicians. - Updates host transfer service endpoints to authorize via
ActionTransferHost(destination and source fleets) instead ofActionWrite. - Expands test coverage across Rego policy tests, service unit tests, integration tests, and frontend UI tests; enables bulk transfer selection in the Manage hosts table for global technicians.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| server/service/integration_mdm_test.go | Updates integration permission expectations to allow global technician transfers; adds team-tech transfer scenarios. |
| server/service/hosts.go | Switches transfer authorization from ActionWrite to ActionTransferHost for destination/source checks. |
| server/service/hosts_test.go | Adds unit tests covering global and team-scoped technician transfer rules (including no-team behavior). |
| server/fleet/authz.go | Adds new ActionTransferHost action constant. |
| server/authz/policy.rego | Adds allow rules for transfer_host for relevant global/team roles. |
| server/authz/policy_test.go | Extends authorization matrix tests to cover transfer_host. |
| frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx | Shows Transfer action for global technicians and enables bulk-selection for transfer while hiding bulk delete. |
| frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx | Adds/updates tests for Transfer visibility for global technicians and ensures unrelated actions stay hidden. |
| frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx | Allows Transfer action for global technicians on Premium (non-Primo mode). |
| changes/41783-technician-transfer-hosts | Adds changelog entry for technician host transfers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR extends host transfer permissions to Technician role in policy and fleet authz, updates service methods to authorize transfers with ActionTransferHost (including source-team checks and No Team), exposes Transfer to global technicians in the frontend (individual and bulk flows), and adds/updates unit, policy, frontend, and integration tests plus a release note. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@changes/41783-technician-transfer-hosts`:
- Line 1: Update the change description to explicitly state that host transfer
is a premium-only feature: modify the added release note sentence ("Added the
ability for users with the Technician role to transfer hosts...") to append a
clear note such as "This functionality is available on premium tiers only; it is
not available on the free tier." Also clarify scope differences (global
technicians via UI and API, fleet-scoped technicians via API) remain unchanged
but are limited to premium customers so users have correct expectations.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f1aa292f-c2b4-4bb3-b1c8-9f8a810cb782
📒 Files selected for processing (10)
changes/41783-technician-transfer-hostsfrontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsxfrontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsxfrontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsxserver/authz/policy.regoserver/authz/policy_test.goserver/fleet/authz.goserver/service/hosts.goserver/service/hosts_test.goserver/service/integration_mdm_test.go
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/service/hosts.go (1)
1243-1251:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMake source-team authorization atomic with the transfer.
Line 1243/Line 1393 read each host's current
TeamID, but the move happens later at Line 1251/Line 1407. A concurrent transfer can change the source team between those steps, so this can authorize against team A and then move the host out of team B. Please enforce the source-team check inside the same datastore transaction/update that performs the transfer, and fail if any host's current team drifted after authorization.Also applies to: 1393-1408
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/service/hosts.go` around lines 1243 - 1251, The source-team check (svc.authorizeHostSourceTeams called after svc.ds.ListHostsLiteByIDs) must be moved into the same datastore operation that performs the transfer to avoid TOCTOU races: modify the datastore update (currently svc.ds.AddHostsToTeam / fleet.NewAddHostsToTeamParams) to accept the expected source team(s) or host->team snapshot and perform an atomic transaction that verifies each host's current TeamID matches the authorized source team(s) and only then updates TeamID to the destination; if any host's TeamID differs, the datastore call must fail and return an error so the service (svc) returns that error instead of proceeding. Use or add a new method like AddHostsToTeamWithSourceCheck or add parameters to AddHostsToTeam, and remove the separate svc.authorizeHostSourceTeams step so authorization and update occur inside the same transactional datastore call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@server/service/hosts.go`:
- Around line 1243-1251: The source-team check (svc.authorizeHostSourceTeams
called after svc.ds.ListHostsLiteByIDs) must be moved into the same datastore
operation that performs the transfer to avoid TOCTOU races: modify the datastore
update (currently svc.ds.AddHostsToTeam / fleet.NewAddHostsToTeamParams) to
accept the expected source team(s) or host->team snapshot and perform an atomic
transaction that verifies each host's current TeamID matches the authorized
source team(s) and only then updates TeamID to the destination; if any host's
TeamID differs, the datastore call must fail and return an error so the service
(svc) returns that error instead of proceeding. Use or add a new method like
AddHostsToTeamWithSourceCheck or add parameters to AddHostsToTeam, and remove
the separate svc.authorizeHostSourceTeams step so authorization and update occur
inside the same transactional datastore call.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: da4eb874-90b2-49e6-9e36-80ab77dda685
📒 Files selected for processing (1)
server/service/hosts.go
nulmete
left a comment
There was a problem hiding this comment.
LGTM, just suggesting some minor tweaks. (Except for the isOnlyObserver -> canSelectHosts prop change, which I think we should tackle separately.)
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/service/hosts_test.go (1)
1961-1972:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTest logic bug: destination team ID doesn't match test intent.
The test comment states "team 1 maintainer tries to transfer it to team 1" and the test name is "cannot steal host from another team", but line 1969 uses
new(uint(2))as the destination, attempting to transfer from team 2 to team 2.To properly test the "stealing" scenario (unauthorized access to source team), the destination should be a team the user CAN write to (team 1), so the failure is specifically due to lack of access to the source team. Compare with the similar test at line 1982 and the technician test at line 2165, which correctly use
new(uint(1)).🐛 Proposed fix
userCtx := test.UserContext(ctx, test.UserTeamMaintainerTeam1) - err := svc.AddHostsToTeam(userCtx, new(uint(2)), []uint{10}, false) + err := svc.AddHostsToTeam(userCtx, new(uint(1)), []uint{10}, false) require.Error(t, err)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/service/hosts_test.go` around lines 1961 - 1972, The test is using the wrong destination team ID so it doesn't exercise the "steal from another team" case; update the destination passed to svc.AddHostsToTeam in the "team maintainer cannot steal host from another team" test to be new(uint(1)) so a user in test.UserTeamMaintainerTeam1 attempts to transfer host ID 10 (which ds.ListHostsLiteByIDsFunc returns with TeamID=2) into team 1; ensure the test still asserts require.Error and that the error contains "forbidden" (references: svc.AddHostsToTeam, ds.ListHostsLiteByIDsFunc, test.UserTeamMaintainerTeam1).
🧹 Nitpick comments (1)
frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx (1)
117-117: 💤 Low valuePrefer
getByTextfor required elements in this assertion path.Using
screen.queryByText("Actions")here returnsHTMLElement | null. If the "Actions" button is ever unexpectedly absent (e.g., a regression hides it for team technicians too),user.click(null)will fail with a less informative error thangetByText's "Unable to find an element with the text: Actions". The other Transfer tests in this block (e.g., Line 90) already usegetByText, so aligning here improves consistency and diagnostics. Note: a previous reviewer suggestedqueryByTextspecifically to drop the if-check — the if-check removal is right, butgetByTextachieves the same intent (failing fast on absence) with a clearer message.♻️ Proposed change
- await user.click(screen.queryByText("Actions")); + await user.click(screen.getByText("Actions"));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx` at line 117, Replace the nullable lookup with a required lookup in the test: in HostActionsDropdown.tests.tsx change the user.click call that uses screen.queryByText("Actions") to use screen.getByText("Actions") so the test fails fast with a clear "Unable to find an element with the text: Actions" message; update the invocation around user.click(screen.getByText("Actions")) and ensure no conditional/null checks remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@server/service/hosts_test.go`:
- Around line 1961-1972: The test is using the wrong destination team ID so it
doesn't exercise the "steal from another team" case; update the destination
passed to svc.AddHostsToTeam in the "team maintainer cannot steal host from
another team" test to be new(uint(1)) so a user in test.UserTeamMaintainerTeam1
attempts to transfer host ID 10 (which ds.ListHostsLiteByIDsFunc returns with
TeamID=2) into team 1; ensure the test still asserts require.Error and that the
error contains "forbidden" (references: svc.AddHostsToTeam,
ds.ListHostsLiteByIDsFunc, test.UserTeamMaintainerTeam1).
---
Nitpick comments:
In
`@frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx`:
- Line 117: Replace the nullable lookup with a required lookup in the test: in
HostActionsDropdown.tests.tsx change the user.click call that uses
screen.queryByText("Actions") to use screen.getByText("Actions") so the test
fails fast with a clear "Unable to find an element with the text: Actions"
message; update the invocation around user.click(screen.getByText("Actions"))
and ensure no conditional/null checks remain.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fa352915-de4e-4da0-a8ae-dbefdffa63ae
📒 Files selected for processing (2)
frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsxserver/service/hosts_test.go
Resolves #41783.
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Testing
Summary by CodeRabbit
New Features
Tests