Added endpoint to fleetdm.com proxy#46107
Conversation
|
@coderabbitai full review |
|
/agentic_review |
✅ Actions performedFull review triggered. |
Code Review by Qodo
1.
|
There was a problem hiding this comment.
Pull request overview
Adds a new fleetdm.com (website) Android proxy endpoint that allows Fleet to issue Android Management API (AMAPI) commands (e.g. LOCK / RESET_PASSWORD / WIPE) against an enterprise-managed device.
Changes:
- Added a new
POST /api/android/v1/enterprises/:androidEnterpriseId/devices/...route for issuing device commands. - Implemented a new Sails controller that authenticates via Fleet server secret and forwards an AMAPI
issueCommandrequest to Google.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| website/config/routes.js | Adds the new Android proxy route for device command issuance. |
| website/api/controllers/android-proxy/issue-command-on-android-device.js | Implements the authenticated proxying logic to Google’s Android Management API enterprises.devices.issueCommand. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'GET /api/android/v1/enterprises/:androidEnterpriseId/devices': { action: 'android-proxy/get-android-devices' }, | ||
| 'DELETE /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/delete-android-device', csrf: false }, | ||
| 'PATCH /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/modify-android-device', csrf: false }, | ||
| 'POST /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId::issueCommand': { action: 'android-proxy/issue-command-on-android-device', csrf: false }, |
| 'GET /api/android/v1/enterprises/:androidEnterpriseId/devices': { action: 'android-proxy/get-android-devices' }, | ||
| 'DELETE /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/delete-android-device', csrf: false }, | ||
| 'PATCH /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/modify-android-device', csrf: false }, | ||
| 'POST /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId::issueCommand': { action: 'android-proxy/issue-command-on-android-device', csrf: false }, |
WalkthroughThis pull request adds a new REST endpoint for issuing Android management commands (lock, wipe, clear passcode) to devices enrolled in an Android Enterprise account managed by Fleet. The controller authenticates via bearer token, validates the Android Enterprise record and its Fleet-management status, constructs an explicit AMAPI 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 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: 3
🤖 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 `@website/api/controllers/android-proxy/issue-command-on-android-device.js`:
- Around line 179-181: Instead of returning an object from the intercept block,
throw the custom exit signal so the action triggers the deviceNoLongerManaged
exit; locate the intercept where it currently does "return
{'deviceNoLongerManaged': 'The device is no longer managed by the Android
enterprise.'};" and replace that return with a throw of the exit object (throw {
deviceNoLongerManaged: 'The device is no longer managed by the Android
enterprise.' }) so the action pipeline invokes the deviceNoLongerManaged exit
properly.
- Around line 173-183: The intercept handlers in
issue-command-on-android-device.js currently interpolate raw error objects (err)
into new Error strings in the 429 handler and the general intercept, which can
leak sensitive upstream details; update both intercept callbacks to build a
sanitized error summary (e.g., extract only err.status/err.code and err.message
or String(err)) and include that sanitized summary in the returned Error or in
the object returned for deviceNoLongerManaged, rather than interpolating the
full err object; reference the existing intercepts (the 429 intercept that logs
via sails.log.warn and returns new Error(...), and the subsequent .intercept
that checks deviceNoLongerManaged) and replace err interpolation with a small
sanitizedError variable composed of permitted fields before including it in the
returned Error message.
- Around line 90-92: The current authHeader parsing (checking
authHeader.startsWith('Bearer') and then replace) is too permissive and
case-sensitive; change the logic around authHeader to strictly match the pattern
"Bearer <token>" using a case-insensitive regex (e.g., /^Bearer\s+(.+)$/i) to
capture the token into fleetServerSecret and otherwise treat the header as
invalid (fall through to the error/rejection branch). Update the block that
references authHeader and fleetServerSecret so only a successful regex match
sets fleetServerSecret; do not accept headers missing the space or with
different casing.
🪄 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: 4bc9f332-aa4d-4dd0-a89e-d01e5f990a24
📒 Files selected for processing (2)
website/api/controllers/android-proxy/issue-command-on-android-device.jswebsite/config/routes.js
| if (authHeader && authHeader.startsWith('Bearer')) { | ||
| fleetServerSecret = authHeader.replace('Bearer', '').trim(); | ||
| } else { |
There was a problem hiding this comment.
Harden Authorization parsing to enforce a valid Bearer format.
Line 90-Line 92 currently accepts malformed headers (e.g., missing whitespace after Bearer) and is case-sensitive. Use strict parsing for Bearer <token> and reject everything else.
Suggested patch
- if (authHeader && authHeader.startsWith('Bearer')) {
- fleetServerSecret = authHeader.replace('Bearer', '').trim();
+ let bearerMatch = authHeader && authHeader.match(/^Bearer\s+(.+)$/i);
+ if (bearerMatch) {
+ fleetServerSecret = bearerMatch[1].trim();
} else {
throw 'missingAuthHeader';
}🤖 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 `@website/api/controllers/android-proxy/issue-command-on-android-device.js`
around lines 90 - 92, The current authHeader parsing (checking
authHeader.startsWith('Bearer') and then replace) is too permissive and
case-sensitive; change the logic around authHeader to strictly match the pattern
"Bearer <token>" using a case-insensitive regex (e.g., /^Bearer\s+(.+)$/i) to
capture the token into fleetServerSecret and otherwise treat the header as
invalid (fall through to the error/rejection branch). Update the block that
references authHeader and fleetServerSecret so only a successful regex match
sets fleetServerSecret; do not accept headers missing the space or with
different casing.
| }).intercept({status: 429}, (err)=>{ | ||
| // If the Android management API returns a 429 response, log an additional warning that will trigger a help-p1 alert. | ||
| sails.log.warn(`p1: Android management API rate limit exceeded!`); | ||
| return new Error(`When attempting to issue a command to a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`); | ||
| }).intercept((err)=>{ | ||
| let errorString = err.toString(); | ||
| if (errorString.includes('Device is no longer being managed')) { | ||
| return {'deviceNoLongerManaged': 'The device is no longer managed by the Android enterprise.'}; | ||
| } | ||
| return new Error(`When attempting to issue a command to a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${require('util').inspect(err)}`); | ||
| }); |
There was a problem hiding this comment.
Avoid propagating raw upstream error objects in message strings.
Line 176 and Line 182 interpolate raw error details, which can leak sensitive request/credential context to logs or responses. Prefer sanitized fields (status + message) only.
Suggested patch
}).intercept({status: 429}, (err)=>{
// If the Android management API returns a 429 response, log an additional warning that will trigger a help-p1 alert.
sails.log.warn(`p1: Android management API rate limit exceeded!`);
- return new Error(`When attempting to issue a command to a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`);
+ return new Error(`Failed to issue Android command for enterprise (${androidEnterpriseId}): ${err && err.message ? err.message : 'rate limited'}`);
}).intercept((err)=>{
let errorString = err.toString();
if (errorString.includes('Device is no longer being managed')) {
throw {deviceNoLongerManaged: 'The device is no longer managed by the Android enterprise.'};
}
- return new Error(`When attempting to issue a command to a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${require('util').inspect(err)}`);
+ return new Error(`Failed to issue Android command for enterprise (${androidEnterpriseId}): ${err && err.message ? err.message : 'unknown error'}`);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| }).intercept({status: 429}, (err)=>{ | |
| // If the Android management API returns a 429 response, log an additional warning that will trigger a help-p1 alert. | |
| sails.log.warn(`p1: Android management API rate limit exceeded!`); | |
| return new Error(`When attempting to issue a command to a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`); | |
| }).intercept((err)=>{ | |
| let errorString = err.toString(); | |
| if (errorString.includes('Device is no longer being managed')) { | |
| return {'deviceNoLongerManaged': 'The device is no longer managed by the Android enterprise.'}; | |
| } | |
| return new Error(`When attempting to issue a command to a device for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${require('util').inspect(err)}`); | |
| }); | |
| }).intercept({status: 429}, (err)=>{ | |
| // If the Android management API returns a 429 response, log an additional warning that will trigger a help-p1 alert. | |
| sails.log.warn(`p1: Android management API rate limit exceeded!`); | |
| return new Error(`Failed to issue Android command for enterprise (${androidEnterpriseId}): ${err && err.message ? err.message : 'rate limited'}`); | |
| }).intercept((err)=>{ | |
| let errorString = err.toString(); | |
| if (errorString.includes('Device is no longer being managed')) { | |
| throw {deviceNoLongerManaged: 'The device is no longer managed by the Android enterprise.'}; | |
| } | |
| return new Error(`Failed to issue Android command for enterprise (${androidEnterpriseId}): ${err && err.message ? err.message : 'unknown error'}`); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website/api/controllers/android-proxy/issue-command-on-android-device.js`
around lines 173 - 183, The intercept handlers in
issue-command-on-android-device.js currently interpolate raw error objects (err)
into new Error strings in the 429 handler and the general intercept, which can
leak sensitive upstream details; update both intercept callbacks to build a
sanitized error summary (e.g., extract only err.status/err.code and err.message
or String(err)) and include that sanitized summary in the returned Error or in
the object returned for deviceNoLongerManaged, rather than interpolating the
full err object; reference the existing intercepts (the 429 intercept that logs
via sails.log.warn and returns new Error(...), and the subsequent .intercept
that checks deviceNoLongerManaged) and replace err interpolation with a small
sanitizedError variable composed of permitted fields before including it in the
returned Error message.
| if (errorString.includes('Device is no longer being managed')) { | ||
| return {'deviceNoLongerManaged': 'The device is no longer managed by the Android enterprise.'}; | ||
| } |
There was a problem hiding this comment.
Throw the custom exit instead of returning an object from intercept.
Line 179-Line 181 returns an object, which can end up as a successful response body instead of triggering the deviceNoLongerManaged exit. Throw the exit signal here.
Suggested patch
if (errorString.includes('Device is no longer being managed')) {
- return {'deviceNoLongerManaged': 'The device is no longer managed by the Android enterprise.'};
+ throw {deviceNoLongerManaged: 'The device is no longer managed by the Android enterprise.'};
}🤖 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 `@website/api/controllers/android-proxy/issue-command-on-android-device.js`
around lines 179 - 181, Instead of returning an object from the intercept block,
throw the custom exit signal so the action triggers the deviceNoLongerManaged
exit; locate the intercept where it currently does "return
{'deviceNoLongerManaged': 'The device is no longer managed by the Android
enterprise.'};" and replace that return with a throw of the exit object (throw {
deviceNoLongerManaged: 'The device is no longer managed by the Android
enterprise.' }) so the action pipeline invokes the deviceNoLongerManaged exit
properly.
Related issue: Resolves #41683
Checklist for submitter
If some of the following don't apply, delete the relevant line.
Changes file added for user-visible changes in
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Timeouts are implemented and retries are limited to avoid infinite loops
If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes
Testing
Added/updated automated tests
Where appropriate, automated tests simulate multiple hosts and test for host isolation (updates to one hosts's records do not affect another)
QA'd all new/changed functionality manually
For unreleased bug fixes in a release candidate, one of:
Database migrations
COLLATE utf8mb4_unicode_ci).New Fleet configuration settings
If you didn't check the box above, follow this checklist for GitOps-enabled settings:
fleetctl generate-gitopsfleetd/orbit/Fleet Desktop
runtime.GOOSis used as needed to isolate changesSummary by CodeRabbit