mirror of
https://github.com/fleetdm/fleet
synced 2026-05-18 14:38:53 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34389 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## 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] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
224 lines
8.2 KiB
Go
224 lines
8.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
|
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/api/androidmanagement/v1"
|
|
)
|
|
|
|
func (s *integrationMDMTestSuite) TestAndroidAppSelfService() {
|
|
ctx := context.Background()
|
|
t := s.T()
|
|
|
|
appConf, err := s.ds.AppConfig(context.Background())
|
|
require.NoError(s.T(), err)
|
|
appConf.MDM.AndroidEnabledAndConfigured = false
|
|
err = s.ds.SaveAppConfig(context.Background(), appConf)
|
|
require.NoError(s.T(), err)
|
|
s.setVPPTokenForTeam(0)
|
|
|
|
t.Cleanup(func() {
|
|
appConf, err := s.ds.AppConfig(context.Background())
|
|
require.NoError(s.T(), err)
|
|
appConf.MDM.AndroidEnabledAndConfigured = true
|
|
err = s.ds.SaveAppConfig(context.Background(), appConf)
|
|
require.NoError(s.T(), err)
|
|
})
|
|
|
|
// Adding android app before android MDM is turned on should fail
|
|
var addAppResp addAppStoreAppResponse
|
|
s.DoJSON(
|
|
"POST",
|
|
"/api/latest/fleet/software/app_store_apps",
|
|
&addAppStoreAppRequest{AppStoreID: "com.should.fail", Platform: fleet.AndroidPlatform},
|
|
http.StatusBadRequest,
|
|
&addAppResp,
|
|
)
|
|
|
|
EnterpriseID := "LC02k5wxw7"
|
|
EnterpriseSignupURL := "https://enterprise.google.com/signup/android/email?origin=android&thirdPartyToken=B4D779F1C4DD9A440"
|
|
s.androidAPIClient.InitCommonMocks()
|
|
|
|
s.androidAPIClient.EnterprisesCreateFunc = func(_ context.Context, _ androidmgmt.EnterprisesCreateRequest) (androidmgmt.EnterprisesCreateResponse, error) {
|
|
return androidmgmt.EnterprisesCreateResponse{
|
|
EnterpriseName: "enterprises/" + EnterpriseID,
|
|
TopicName: "projects/android/topics/ae98ed130-5ce2-4ddb-a90a-191ec76976d5",
|
|
}, nil
|
|
}
|
|
s.androidAPIClient.EnterprisesPoliciesPatchFunc = func(_ context.Context, policyName string, _ *androidmanagement.Policy) (*androidmanagement.Policy, error) {
|
|
assert.Contains(t, policyName, EnterpriseID)
|
|
return &androidmanagement.Policy{}, nil
|
|
}
|
|
s.androidAPIClient.EnterpriseDeleteFunc = func(_ context.Context, enterpriseName string) error {
|
|
assert.Equal(t, "enterprises/"+EnterpriseID, enterpriseName)
|
|
return nil
|
|
}
|
|
|
|
s.androidAPIClient.SignupURLsCreateFunc = func(_ context.Context, _, callbackURL string) (*android.SignupDetails, error) {
|
|
s.proxyCallbackURL = callbackURL
|
|
return &android.SignupDetails{
|
|
Url: EnterpriseSignupURL,
|
|
Name: "signupUrls/Cb08124d0999c464f",
|
|
}, nil
|
|
}
|
|
|
|
s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFunc = func(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error) {
|
|
return &androidmanagement.Policy{}, nil
|
|
}
|
|
|
|
s.androidAPIClient.EnterprisesDevicesPatchFunc = func(ctx context.Context, deviceName string, device *androidmanagement.Device) (*androidmanagement.Device, error) {
|
|
return &androidmanagement.Device{}, nil
|
|
}
|
|
|
|
// Create enterprise
|
|
var signupResp android.EnterpriseSignupResponse
|
|
s.DoJSON("GET", "/api/v1/fleet/android_enterprise/signup_url", nil, http.StatusOK, &signupResp)
|
|
|
|
const enterpriseToken = "enterpriseToken"
|
|
|
|
// callback URL includes the host, need to extract the path so we can call it with our
|
|
// HTTP request helpers
|
|
u, err := url.Parse(s.proxyCallbackURL)
|
|
require.NoError(t, err)
|
|
s.Do("GET", u.Path, nil, http.StatusOK, "enterpriseToken", enterpriseToken)
|
|
|
|
// Update the LIST mock to return the enterprise after "creation"
|
|
s.androidAPIClient.EnterprisesListFunc = func(_ context.Context, _ string) ([]*androidmanagement.Enterprise, error) {
|
|
return []*androidmanagement.Enterprise{
|
|
{Name: "enterprises/" + EnterpriseID},
|
|
}, nil
|
|
}
|
|
|
|
resp := android.GetEnterpriseResponse{}
|
|
s.DoJSON("GET", "/api/v1/fleet/android_enterprise", nil, http.StatusOK, &resp)
|
|
assert.Equal(t, EnterpriseID, resp.EnterpriseID)
|
|
|
|
// Android MDM setup
|
|
androidApp := &fleet.VPPApp{
|
|
VPPAppTeam: fleet.VPPAppTeam{
|
|
VPPAppID: fleet.VPPAppID{
|
|
AdamID: "com.whatsapp",
|
|
Platform: fleet.AndroidPlatform,
|
|
},
|
|
},
|
|
Name: "WhatsApp",
|
|
BundleIdentifier: "com.whatsapp",
|
|
IconURL: "https://example.com/images/2",
|
|
}
|
|
|
|
// Invalid application ID format: should fail
|
|
r := s.Do(
|
|
"POST",
|
|
"/api/latest/fleet/software/app_store_apps",
|
|
&addAppStoreAppRequest{AppStoreID: "thisisnotanappid", Platform: fleet.AndroidPlatform},
|
|
http.StatusUnprocessableEntity,
|
|
)
|
|
require.Contains(t, extractServerErrorText(r.Body), "app_store_id must be a valid Android application ID")
|
|
|
|
// Missing platform: should fail
|
|
r = s.Do(
|
|
"POST",
|
|
"/api/latest/fleet/software/app_store_apps",
|
|
&addAppStoreAppRequest{AppStoreID: "com.valid.app.id"},
|
|
http.StatusUnprocessableEntity,
|
|
)
|
|
require.Contains(t, extractServerErrorText(r.Body), "Error: Couldn't add software. com.valid.app.id isn't available in Apple Business Manager. Please purchase license in Apple Business Manager and try again.")
|
|
|
|
// Valid application ID format, but app isn't found: should fail
|
|
// Update mock to return a 404
|
|
s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
|
|
return nil, ¬FoundError{}
|
|
}
|
|
|
|
r = s.Do(
|
|
"POST",
|
|
"/api/latest/fleet/software/app_store_apps",
|
|
&addAppStoreAppRequest{AppStoreID: "com.app.id.not.found", Platform: fleet.AndroidPlatform},
|
|
http.StatusUnprocessableEntity,
|
|
)
|
|
require.Contains(t, extractServerErrorText(r.Body), "Couldn't add software. The application ID isn't available in Play Store. Please find ID on the Play Store and try again.")
|
|
|
|
s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
|
|
return &androidmanagement.Application{IconUrl: "https://example.com/1.jpg", Title: "Test App"}, nil
|
|
}
|
|
|
|
// Add Android app
|
|
s.DoJSON(
|
|
"POST",
|
|
"/api/latest/fleet/software/app_store_apps",
|
|
&addAppStoreAppRequest{AppStoreID: androidApp.AdamID, Platform: fleet.AndroidPlatform},
|
|
http.StatusOK,
|
|
&addAppResp,
|
|
)
|
|
|
|
secrets, err := s.ds.GetEnrollSecrets(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, secrets, 1)
|
|
|
|
assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAndroidPubSubToken}, nil)
|
|
require.NoError(t, err)
|
|
pubsubToken := assets[fleet.MDMAssetAndroidPubSubToken]
|
|
require.NotEmpty(t, pubsubToken.Value)
|
|
|
|
deviceID1 := createAndroidDeviceID("test-android")
|
|
deviceID2 := createAndroidDeviceID("test-android-2")
|
|
|
|
enterpriseSpecificID1 := strings.ToUpper(uuid.New().String())
|
|
enterpriseSpecificID2 := strings.ToUpper(uuid.New().String())
|
|
var req android_service.PubSubPushRequest
|
|
for _, d := range []struct {
|
|
id string
|
|
esi string
|
|
}{{deviceID1, enterpriseSpecificID1}, {deviceID2, enterpriseSpecificID2}} {
|
|
enrollmentMessage := enrollmentMessageWithEnterpriseSpecificID(
|
|
t,
|
|
androidmanagement.Device{
|
|
Name: d.id,
|
|
EnrollmentTokenData: fmt.Sprintf(`{"EnrollSecret": "%s"}`, secrets[0].Secret),
|
|
},
|
|
d.esi,
|
|
)
|
|
|
|
req = android_service.PubSubPushRequest{
|
|
PubSubMessage: *enrollmentMessage,
|
|
}
|
|
|
|
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
|
|
}
|
|
|
|
var hosts listHostsResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &hosts)
|
|
|
|
assert.Len(t, hosts.Hosts, 2)
|
|
|
|
host1 := hosts.Hosts[0]
|
|
assert.Equal(t, host1.Platform, string(fleet.AndroidPlatform))
|
|
|
|
// Should see it in host software library
|
|
getHostSw := getHostSoftwareResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host1.ID), nil, http.StatusOK, &getHostSw, "available_for_install", "true")
|
|
assert.Len(t, getHostSw.Software, 1)
|
|
s.Assert().NotNil(getHostSw.Software[0].AppStoreApp)
|
|
s.Assert().Equal(androidApp.AdamID, getHostSw.Software[0].AppStoreApp.AppStoreID)
|
|
|
|
// Google AMAPI hasn't been hit yet
|
|
s.Assert().False(s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
|
|
|
|
// Run worker, should run the job that assigns the app to the host's MDM policy
|
|
s.runWorkerUntilDone()
|
|
|
|
// Should have hit the android API endpoint
|
|
s.Assert().True(s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked)
|
|
|
|
}
|