Files
Victor Lyuboslavsky e790260b85 Android commands backend (#46031)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41683 

Support for Android lock, wipe, and clear passcode commands. Behavior is
slightly different between BYOD and CODO. The fleetdm.com proxy isn't
wired up, so they only work with direct Google connection.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Clear-passcode CLI plus Android Lock and Wipe commands (Wipe
restricted to company-owned devices).
* BYO unenroll now removes only the work profile, preserving personal
data.
* Commands issued with a 10-year duration; UI/CLI show Android-specific
messaging and command IDs.

* **Improvements**
* Host MDM pages reflect command lifecycle transitions (pending →
acknowledged or error with code/message) via Pub/Sub updates.

* **Documentation**
* Updated docs for Android MDM commands, ownership rules, and command
duration.

* **Tests**
  * New unit and integration tests for Android MDM flows.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/46031?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-26 12:16:03 -05:00

103 lines
5.5 KiB
Go

package android
import (
"context"
"net/http"
"google.golang.org/api/androidmanagement/v1"
)
type Service interface {
EnterpriseSignup(ctx context.Context) (*SignupDetails, error)
EnterpriseSignupCallback(ctx context.Context, signupToken string, enterpriseToken string) error
GetEnterprise(ctx context.Context) (*Enterprise, error)
DeleteEnterprise(ctx context.Context) error
EnterpriseSignupSSE(ctx context.Context) (chan string, error)
// CreateEnrollmentToken creates an enrollment token for a new Android device.
CreateEnrollmentToken(ctx context.Context, enrollSecret, idpUUID string, fullyManaged bool) (*EnrollmentToken, error)
ProcessPubSubPush(ctx context.Context, token string, message *PubSubMessage) error
// UnenrollAndroidHost triggers unenrollment (work profile removal) for the given Android host ID.
UnenrollAndroidHost(ctx context.Context, hostID uint) error
// LockAndroidHost issues an AMAPI LOCK command for the given host, persists the row in mdm_android_commands
// (status=pending), and writes host_mdm_actions.lock_ref. The device-side ack arrives asynchronously via Pub/Sub
// COMMAND notification and is applied by ProcessPubSubPush.
LockAndroidHost(ctx context.Context, hostID uint) error
// ClearAndroidPasscode issues an AMAPI RESET_PASSWORD command with newPassword="". The work-profile (BYO)
// or device (COBO) passcode is cleared, not regenerated. Persists the row in mdm_android_commands but does NOT touch
// host_mdm_actions (clear passcode is a one-shot action with no UI lock/wipe state). Returns the Fleet-generated
// command_uuid so callers can correlate the API response with the persisted row via GetMDMAndroidCommandByUUID.
ClearAndroidPasscode(ctx context.Context, hostID uint) (commandUUID string, err error)
// WipeAndroidHost issues an AMAPI WIPE command. COBO-only; callers in the service layer reject BYO before reaching
// here. Persists the row in mdm_android_commands and writes host_mdm_actions.wipe_ref.
WipeAndroidHost(ctx context.Context, hostID uint) error
EnterprisesApplications(ctx context.Context, enterpriseName, applicationID string) (*androidmanagement.Application, error)
AddAppsToAndroidPolicy(ctx context.Context, enterpriseName string, appPolicies []*androidmanagement.ApplicationPolicy, hostUUIDs map[string]string) (map[string]*MDMAndroidPolicyRequest, error)
RemoveAppsFromAndroidPolicy(ctx context.Context, enterpriseName string, packageNames []string, hostUUIDs map[string]string) (map[string]*MDMAndroidPolicyRequest, error)
// SetAppsForAndroidPolicy sets the available apps for the given hosts' Android MDM policy to the given list of apps.
// Note that unlike AddAppsToAndroidPolicy, this method replaces the existing app list with the given one, it is
// not additive/PATCH semantics.
SetAppsForAndroidPolicy(ctx context.Context, enterpriseName string, appPolicies []*androidmanagement.ApplicationPolicy, hostUUIDs map[string]string) error
AddFleetAgentToAndroidPolicy(ctx context.Context, enterpriseName string, hostConfigs map[string]AgentManagedConfiguration) error
BuildFleetAgentApplicationPolicy(ctx context.Context, hostUUID string) (*androidmanagement.ApplicationPolicy, error)
// BuildAndSendFleetAgentConfig builds the complete AgentManagedConfiguration for the given hosts
// (including certificate templates) and sends it to the Android Management API.
// This is the centralized function that should be used by all callers to avoid race conditions.
// If skipHostsWithoutNewCerts is true, hosts that don't have new certificate templates to deliver
// will be skipped.
BuildAndSendFleetAgentConfig(ctx context.Context, enterpriseName string, hostUUIDs []string, skipHostsWithoutNewCerts bool) error
EnableAppReportsOnDefaultPolicy(ctx context.Context) error
MigrateToPerDevicePolicy(ctx context.Context) error
PatchDevice(ctx context.Context, policyID, deviceName string, device *androidmanagement.Device) (skip bool, apiErr error)
PatchPolicy(ctx context.Context, policyID, policyName string, policy *androidmanagement.Policy, metadata map[string]string) (skip bool, err error)
// VerifyExistingEnterpriseIfAny checks if there's an existing enterprise in the database
// and if so, verifies it still exists in Google API. If it doesn't exist, performs cleanup.
// Returns fleet.IsNotFound error if enterprise was deleted, nil if no enterprise exists or verification passed.
VerifyExistingEnterpriseIfAny(ctx context.Context) error
// CreateAndroidWebApp creates a new web app for the given enterprise.
CreateAndroidWebApp(ctx context.Context, enterpriseName string, app *androidmanagement.WebApp) (*androidmanagement.WebApp, error)
}
// /////////////////////////////////////////////
// Android API request and response structs
type DefaultResponse struct {
Err error `json:"error,omitempty"`
}
func (r DefaultResponse) Error() error { return r.Err }
// StatusCode implements the go-kit http StatusCoder interface to preserve HTTP status codes from errors
func (r DefaultResponse) StatusCode() int {
if r.Err != nil {
// Check if the error has a custom status code (like errors created with .WithStatus())
if sc, ok := r.Err.(interface{ StatusCode() int }); ok {
return sc.StatusCode()
}
}
// Default to 200 OK if no error or no custom status code
return http.StatusOK
}
type GetEnterpriseResponse struct {
EnterpriseID string `json:"android_enterprise_id"`
DefaultResponse
}
type EnterpriseSignupResponse struct {
Url string `json:"android_enterprise_signup_url"`
DefaultResponse
}
type EnrollmentTokenResponse struct {
*EnrollmentToken
DefaultResponse
}