Android app self service: backend support (#34711)

<!-- 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`).
This commit is contained in:
Jahziel Villasana-Espinoza
2025-11-13 18:10:24 -05:00
committed by GitHub
parent 41a163119b
commit ff0ba413b7
58 changed files with 1795 additions and 315 deletions
+7 -1
View File
@@ -663,6 +663,7 @@ func newWorkerIntegrationsSchedule(
commander *apple_mdm.MDMAppleCommander,
bootstrapPackageStore fleet.MDMBootstrapPackageStore,
vppInstaller fleet.AppleMDMVPPInstaller,
androidModule android.Service,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronWorkerIntegrations)
@@ -726,7 +727,12 @@ func newWorkerIntegrationsSchedule(
Datastore: ds,
Log: logger,
}
w.Register(jira, zendesk, macosSetupAsst, appleMDM, dbMigrate, vppVerify)
softwareWorker := &worker.SoftwareWorker{
Datastore: ds,
Log: logger,
AndroidModule: androidModule,
}
w.Register(jira, zendesk, macosSetupAsst, appleMDM, dbMigrate, vppVerify, softwareWorker)
// Read app config a first time before starting, to clear up any failer client
// configuration if we're not on a fleet-owned server. Technically, the ServerURL
+5 -2
View File
@@ -63,6 +63,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service/conditional_access_microsoft_proxy"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
otelmw "github.com/fleetdm/fleet/v4/server/service/middleware/otel"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
"github.com/fleetdm/fleet/v4/server/service/redis_policy_set"
@@ -785,14 +786,15 @@ the way that the Fleet server works.
if err != nil {
initFatal(err, "initializing service")
}
activitiesModule := activities.NewActivityModule(ds, logger)
androidSvc, err := android_service.NewService(
ctx,
logger,
ds,
svc,
config.License.Key,
config.Server.PrivateKey,
ds,
activitiesModule,
)
if err != nil {
initFatal(err, "initializing android service")
@@ -897,6 +899,7 @@ the way that the Fleet server works.
redis_key_value.New(redisPool),
scepConfigMgr,
digiCertService,
androidSvc,
hydrantService,
)
if err != nil {
@@ -1024,7 +1027,7 @@ the way that the Fleet server works.
if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
vppInstaller := svc.(fleet.AppleMDMVPPInstaller)
return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, bootstrapPackageStore, vppInstaller)
return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, bootstrapPackageStore, vppInstaller, androidSvc)
}); err != nil {
initFatal(err, "failed to register worker integrations schedule")
}
+1
View File
@@ -122,6 +122,7 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser
nil,
nil,
nil,
nil,
)
if err != nil {
panic(err)
+4
View File
@@ -7,6 +7,7 @@ import (
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
"github.com/fleetdm/fleet/v4/server/sso"
@@ -34,6 +35,7 @@ type Service struct {
keyValueStore fleet.KeyValueStore
scepConfigService fleet.SCEPConfigService
digiCertService fleet.DigiCertService
androidModule android.Service
estService fleet.ESTService
}
@@ -55,6 +57,7 @@ func NewService(
keyValueStore fleet.KeyValueStore,
scepConfigService fleet.SCEPConfigService,
digiCertService fleet.DigiCertService,
androidService android.Service,
estService fleet.ESTService,
) (*Service, error) {
authorizer, err := authz.NewAuthorizer()
@@ -81,6 +84,7 @@ func NewService(
keyValueStore: keyValueStore,
scepConfigService: scepConfigService,
digiCertService: digiCertService,
androidModule: androidService,
estService: estService,
}
+4 -4
View File
@@ -1116,7 +1116,7 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
}
platform := host.FleetPlatform()
mobileAppleDevice := fleet.AppleDevicePlatform(platform) == fleet.IOSPlatform || fleet.AppleDevicePlatform(platform) == fleet.IPadOSPlatform
mobileAppleDevice := fleet.InstallableDevicePlatform(platform) == fleet.IOSPlatform || fleet.InstallableDevicePlatform(platform) == fleet.IPadOSPlatform
if !mobileAppleDevice && (host.OrbitNodeKey == nil || *host.OrbitNodeKey == "") {
// fleetd is required to install software so if the host is
@@ -1239,7 +1239,7 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
}
}
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.InstallableDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
SelfService: false,
})
return err
@@ -2564,9 +2564,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f
}
platform := host.FleetPlatform()
mobileAppleDevice := fleet.AppleDevicePlatform(platform) == fleet.IOSPlatform || fleet.AppleDevicePlatform(platform) == fleet.IPadOSPlatform
mobileAppleDevice := fleet.InstallableDevicePlatform(platform) == fleet.IOSPlatform || fleet.InstallableDevicePlatform(platform) == fleet.IPadOSPlatform
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.InstallableDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
SelfService: true,
})
return err
+110 -64
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
"time"
@@ -17,6 +18,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/itunes"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/go-kit/log/level"
)
@@ -314,8 +316,8 @@ func (svc *Service) GetAppStoreApps(ctx context.Context, teamID *uint) ([]*fleet
return apps, nil
}
func getPlatformsFromSupportedDevices(supportedDevices []string) map[fleet.AppleDevicePlatform]struct{} {
platforms := make(map[fleet.AppleDevicePlatform]struct{}, 1)
func getPlatformsFromSupportedDevices(supportedDevices []string) map[fleet.InstallableDevicePlatform]struct{} {
platforms := make(map[fleet.InstallableDevicePlatform]struct{}, 1)
if len(supportedDevices) == 0 {
platforms[fleet.MacOSPlatform] = struct{}{}
return platforms
@@ -334,6 +336,8 @@ func getPlatformsFromSupportedDevices(supportedDevices []string) map[fleet.Apple
return platforms
}
var androidApplicationID = regexp.MustCompile(`^([A-Za-z]{1}[A-Za-z\d_]*\.)+[A-Za-z][A-Za-z\d_]*$`)
func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID fleet.VPPAppTeam) (uint, error) {
if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil {
return 0, err
@@ -350,9 +354,10 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
if appID.Platform == "" {
appID.Platform = fleet.MacOSPlatform
}
if appID.Platform != fleet.IOSPlatform && appID.Platform != fleet.IPadOSPlatform && appID.Platform != fleet.MacOSPlatform {
if !appID.Platform.IsValidInstallableDevicePlatform() {
return 0, fleet.NewInvalidArgumentError("platform",
fmt.Sprintf("platform must be one of '%s', '%s', or '%s", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform))
fmt.Sprintf("platform must be one of '%s', '%s', '%s', or '%s'", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform, fleet.AndroidPlatform))
}
validatedLabels, err := ValidateSoftwareLabels(ctx, svc, appID.LabelsIncludeAny, appID.LabelsExcludeAny)
@@ -377,73 +382,112 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
return 0, fleet.NewUserMessageError(errors.New("Currently, automatic install is only supported on macOS, Windows, and Linux. Please add the app without automatic_install and manually install it on the Host details page."), http.StatusBadRequest)
}
vppToken, err := svc.getVPPToken(ctx, teamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "retrieving VPP token")
}
var app *fleet.VPPApp
assets, err := vpp.GetAssets(ctx, vppToken, &vpp.AssetFilter{AdamID: appID.AdamID})
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "retrieving VPP asset")
}
// Different flows based on platform
switch appID.Platform {
case fleet.AndroidPlatform:
if !androidApplicationID.MatchString(appID.AdamID) {
return 0, fleet.NewInvalidArgumentError("app_store_id", "app_store_id must be a valid Android application ID")
}
appID.SelfService = true
appID.AddAutoInstallPolicy = false
if len(assets) == 0 {
return 0, ctxerr.New(ctx,
fmt.Sprintf("Error: Couldn't add software. %s isn't available in Apple Business Manager. Please purchase license in Apple Business Manager and try again.",
appID.AdamID))
}
asset := assets[0]
assetMetadata, err := itunes.GetAssetMetadata([]string{asset.AdamID}, &itunes.AssetMetadataFilter{Entity: "software"})
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "fetching VPP asset metadata")
}
assetMD := assetMetadata[asset.AdamID]
platforms := getPlatformsFromSupportedDevices(assetMD.SupportedDevices)
if _, ok := platforms[appID.Platform]; !ok {
return 0, fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("%s isn't available for %s", assetMD.TrackName, appID.Platform))
}
if appID.Platform == fleet.MacOSPlatform {
// Check if we've already added an installer for this app
exists, err := svc.ds.UploadedSoftwareExists(ctx, assetMD.BundleID, teamID)
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking existence of VPP app installer")
return 0, &fleet.BadRequestError{Message: "Android MDM is not enabled", InternalErr: err}
}
if exists {
return 0, ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.CantAddSoftwareConflictMessage,
assetMD.TrackName, teamName),
}, "vpp app conflicts with existing software installer")
androidApp, err := svc.androidModule.EnterprisesApplications(ctx, enterprise.Name(), appID.AdamID)
if err != nil {
if fleet.IsNotFound(err) {
return 0, fleet.NewInvalidArgumentError("app_store_id", "Couldn't add software. The application ID isn't available in Play Store. Please find ID on the Play Store and try again.")
}
return 0, ctxerr.Wrap(ctx, err, "add app store app: check if android app exists")
}
}
appID.ValidatedLabels = validatedLabels
appID.Categories = server.RemoveDuplicatesFromSlice(appID.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, appID.Categories)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "getting software category ids")
}
if len(catIDs) != len(appID.Categories) {
return 0, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", appID.Categories),
app = &fleet.VPPApp{
VPPAppTeam: appID,
BundleIdentifier: appID.AdamID,
IconURL: androidApp.IconUrl,
Name: androidApp.Title,
TeamID: teamID,
}
err = worker.QueueMakeAndroidAppAvailableJob(context.Background(), svc.ds, svc.logger, appID.AdamID, app.AppTeamID, enterprise.Name())
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "enqueuing job to make android app available")
}
default:
vppToken, err := svc.getVPPToken(ctx, teamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "retrieving VPP token")
}
assets, err := vpp.GetAssets(ctx, vppToken, &vpp.AssetFilter{AdamID: appID.AdamID})
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "retrieving VPP asset")
}
if len(assets) == 0 {
return 0, fleet.NewInvalidArgumentError("app_store_id",
fmt.Sprintf("Error: Couldn't add software. %s isn't available in Apple Business Manager. Please purchase license in Apple Business Manager and try again.", appID.AdamID))
}
asset := assets[0]
assetMetadata, err := itunes.GetAssetMetadata([]string{asset.AdamID}, &itunes.AssetMetadataFilter{Entity: "software"})
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "fetching VPP asset metadata")
}
assetMD := assetMetadata[asset.AdamID]
platforms := getPlatformsFromSupportedDevices(assetMD.SupportedDevices)
if _, ok := platforms[appID.Platform]; !ok {
return 0, fleet.NewInvalidArgumentError("app_store_id", fmt.Sprintf("%s isn't available for %s", assetMD.TrackName, appID.Platform))
}
if appID.Platform == fleet.MacOSPlatform {
// Check if we've already added an installer for this app
exists, err := svc.ds.UploadedSoftwareExists(ctx, assetMD.BundleID, teamID)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "checking existence of VPP app installer")
}
if exists {
return 0, ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.CantAddSoftwareConflictMessage,
assetMD.TrackName, teamName),
}, "vpp app conflicts with existing software installer")
}
}
appID.ValidatedLabels = validatedLabels
appID.Categories = server.RemoveDuplicatesFromSlice(appID.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, appID.Categories)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "getting software category ids")
}
if len(catIDs) != len(appID.Categories) {
return 0, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", appID.Categories),
}
}
appID.CategoryIDs = catIDs
app = &fleet.VPPApp{
VPPAppTeam: appID,
BundleIdentifier: assetMD.BundleID,
IconURL: assetMD.ArtworkURL,
Name: assetMD.TrackName,
LatestVersion: assetMD.Version,
}
}
appID.CategoryIDs = catIDs
app := &fleet.VPPApp{
VPPAppTeam: appID,
BundleIdentifier: assetMD.BundleID,
IconURL: assetMD.ArtworkURL,
Name: assetMD.TrackName,
LatestVersion: assetMD.Version,
}
addedApp, err := svc.ds.InsertVPPAppWithTeam(ctx, app, teamID)
@@ -464,6 +508,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
LabelsIncludeAny: actLabelsIncl,
LabelsExcludeAny: actLabelsExcl,
}
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil {
return 0, ctxerr.Wrap(ctx, err, "create activity for add app store app")
}
@@ -481,6 +526,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee
}
return addedApp.TitleID, nil
}
func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.VPPApp, error) {
@@ -488,10 +534,10 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V
// Map of adamID to platform, then to whether it's available as self-service
// and installed during setup.
adamIDMap := make(map[string]map[fleet.AppleDevicePlatform]fleet.VPPAppTeam)
adamIDMap := make(map[string]map[fleet.InstallableDevicePlatform]fleet.VPPAppTeam)
for _, id := range ids {
if _, ok := adamIDMap[id.AdamID]; !ok {
adamIDMap[id.AdamID] = make(map[fleet.AppleDevicePlatform]fleet.VPPAppTeam, 1)
adamIDMap[id.AdamID] = make(map[fleet.InstallableDevicePlatform]fleet.VPPAppTeam, 1)
adamIDMap[id.AdamID][id.Platform] = fleet.VPPAppTeam{
SelfService: id.SelfService,
InstallDuringSetup: id.InstallDuringSetup,
BIN
View File
Binary file not shown.
+14
View File
@@ -1517,3 +1517,17 @@ func (ds *Datastore) ListAndroidEnrolledDevicesForReconcile(ctx context.Context)
}
return devices, nil
}
func isAndroidHostConnectedToFleetMDM(ctx context.Context, q sqlx.QueryerContext, h *fleet.Host) (bool, error) {
var isEnrolled bool
err := sqlx.GetContext(ctx, q, &isEnrolled, `
SELECT 1 FROM host_mdm
WHERE host_id = ? AND enrolled = 1
`, h.ID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "check android host mdm enrolled")
}
return isEnrolled, nil
}
+16 -6
View File
@@ -92,10 +92,20 @@ func (ds *AndroidDatastore) DeleteOtherEnterprises(ctx context.Context, id uint)
}
func (ds *AndroidDatastore) DeleteAllEnterprises(ctx context.Context) error {
stmt := `DELETE FROM android_enterprises`
_, err := ds.Writer(ctx).ExecContext(ctx, stmt)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting all enterprises")
}
return nil
return ds.WithTxx(ctx, func(tx sqlx.ExtContext) error {
stmt := `DELETE FROM android_enterprises`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting all enterprises")
}
// Aligns Fleet's state with the AMAPI state
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_apps_teams WHERE platform = 'android'`)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting all android app store apps")
}
return nil
})
}
+4
View File
@@ -47,3 +47,7 @@ func (ds *AndroidDatastore) Writer(_ context.Context) *sqlx.DB {
func (ds *AndroidDatastore) WithRetryTxx(ctx context.Context, fn common_mysql.TxFn) (err error) {
return common_mysql.WithRetryTxx(ctx, ds.Writer(ctx), fn, ds.logger)
}
func (ds *AndroidDatastore) WithTxx(ctx context.Context, fn common_mysql.TxFn) (err error) {
return common_mysql.WithTxx(ctx, ds.Writer(ctx), fn, ds.logger)
}
+8 -4
View File
@@ -1847,13 +1847,17 @@ func (ds *Datastore) AreHostsConnectedToFleetMDM(ctx context.Context, hosts []*f
}
func (ds *Datastore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) {
if host.Platform == "windows" {
switch host.Platform {
case "windows":
return isWindowsHostConnectedToFleetMDM(ctx, ds.reader(ctx), host)
} else if host.Platform == "darwin" || host.Platform == "ipados" || host.Platform == "ios" {
case "darwin", "ipados", "ios":
return isAppleHostConnectedToFleetMDM(ctx, ds.reader(ctx), host)
case "android":
// Android hosts can only enroll via MDM
return isAndroidHostConnectedToFleetMDM(ctx, ds.reader(ctx), host)
default:
return false, nil
}
return false, nil
}
func batchSetProfileVariableAssociationsDB(
@@ -24,7 +24,7 @@ func TestUp_20240730374423(t *testing.T) {
// Apply current migration.
applyNext(t, db)
var platform fleet.AppleDevicePlatform
var platform fleet.InstallableDevicePlatform
require.NoError(t, db.Get(&platform, `SELECT platform FROM vpp_apps WHERE adam_id = ?`, adamID))
assert.Equal(t, fleet.MacOSPlatform, platform)
@@ -0,0 +1,81 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20251112191443, Down_20251112191443)
}
func Up_20251112191443(tx *sql.Tx) error {
_, err := tx.Exec(`ALTER TABLE vpp_apps_teams DROP FOREIGN KEY fk_vpp_apps_teams_vpp_token_id`)
if err != nil {
return fmt.Errorf("failed to drop fk from vpp_apps_table: %w", err)
}
_, err = tx.Exec(`ALTER TABLE vpp_apps_teams MODIFY vpp_token_id INT UNSIGNED`)
if err != nil {
return fmt.Errorf("failed to make vpp_apps_table.vpp_token_id nullable: %w", err)
}
// Drop all FK constraints so we can modify the column size
_, err = tx.Exec(`ALTER TABLE vpp_app_upcoming_activities DROP CONSTRAINT fk_vpp_app_upcoming_activities_adam_id_platform`)
if err != nil {
return fmt.Errorf("failed to drop vpp_app_upcoming_activities.adam_id fk: %w", err)
}
_, err = tx.Exec(`ALTER TABLE host_vpp_software_installs DROP CONSTRAINT host_vpp_software_installs_ibfk_3`)
if err != nil {
return fmt.Errorf("failed to drop host_vpp_software_installs.adam_id fk: %w", err)
}
_, err = tx.Exec(`ALTER TABLE vpp_apps_teams DROP CONSTRAINT vpp_apps_teams_ibfk_3`)
if err != nil {
return fmt.Errorf("failed to drop vpp_apps_teams.adam_id fk: %w", err)
}
// Do the actual column size modification
_, err = tx.Exec(`ALTER TABLE vpp_apps MODIFY COLUMN adam_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL`)
if err != nil {
return fmt.Errorf("failed to increase size of vpp_apps.adam_id: %w", err)
}
_, err = tx.Exec(`ALTER TABLE vpp_app_upcoming_activities MODIFY COLUMN adam_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL`)
if err != nil {
return fmt.Errorf("failed to increase size of vpp_app_upcoming_activities.adam_id: %w", err)
}
_, err = tx.Exec(`ALTER TABLE host_vpp_software_installs MODIFY COLUMN adam_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL`)
if err != nil {
return fmt.Errorf("failed to increase size of host_vpp_software_installs .adam_id: %w", err)
}
_, err = tx.Exec(`ALTER TABLE vpp_apps_teams MODIFY COLUMN adam_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL`)
if err != nil {
return fmt.Errorf("failed to increase size of vpp_apps_teams.adam_id: %w", err)
}
// Add back all the FKs we deleted above
_, err = tx.Exec(`ALTER TABLE vpp_app_upcoming_activities ADD CONSTRAINT fk_vpp_app_upcoming_activities_adam_id_platform FOREIGN KEY (adam_id, platform) REFERENCES vpp_apps (adam_id, platform)`)
if err != nil {
return fmt.Errorf("failed to add vpp_app_upcoming_activities.adam_id fk: %w", err)
}
_, err = tx.Exec(`ALTER TABLE host_vpp_software_installs ADD CONSTRAINT host_vpp_software_installs_ibfk_3 FOREIGN KEY (adam_id, platform) REFERENCES vpp_apps (adam_id, platform) ON DELETE CASCADE`)
if err != nil {
return fmt.Errorf("failed to add host_vpp_software_installs.adam_id fk: %w", err)
}
_, err = tx.Exec(`ALTER TABLE vpp_apps_teams ADD CONSTRAINT vpp_apps_teams_ibfk_3 FOREIGN KEY (adam_id, platform) REFERENCES vpp_apps (adam_id, platform) ON DELETE CASCADE`)
if err != nil {
return fmt.Errorf("failed to add vpp_apps_teams.adam_id fk: %w", err)
}
return nil
}
func Down_20251112191443(tx *sql.Tx) error {
return nil
}
@@ -0,0 +1,35 @@
package tables
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUp_20251112191443(t *testing.T) {
db := applyUpToPrev(t)
// Apply current migration.
applyNext(t, db)
// These tables should still have the FKs they had before the migration
for tableName, fkName := range map[string]string{
"vpp_app_upcoming_activities": "fk_vpp_app_upcoming_activities_adam_id_platform",
"host_vpp_software_installs": "host_vpp_software_installs_ibfk_3",
"vpp_apps_teams": "vpp_apps_teams_ibfk_3",
} {
var columnNames []string
err := db.Select(&columnNames, `
SELECT
COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
REFERENCED_TABLE_SCHEMA = (SELECT DATABASE()) AND
TABLE_NAME = ? AND CONSTRAINT_NAME = ?`, tableName, fkName)
require.NoError(t, err)
assert.ElementsMatch(t, columnNames, []string{"adam_id", "platform"})
}
}
+4
View File
@@ -30,6 +30,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android"
nano_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/go-sql-driver/mysql"
@@ -38,6 +39,9 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
// Compile-time interface check
var _ activities.ActivityStore = (*Datastore)(nil)
const (
defaultSelectLimit = 1000000
mySQLTimestampFormat = "2006-01-02 15:04:05" // %Y/%m/%d %H:%M:%S
File diff suppressed because one or more lines are too long
+34 -1
View File
@@ -840,7 +840,7 @@ WHERE
if row.VPPAdamID.Valid {
vppAppID = &fleet.VPPAppID{
AdamID: row.VPPAdamID.V,
Platform: fleet.AppleDevicePlatform(row.VPPPlatform.V),
Platform: fleet.InstallableDevicePlatform(row.VPPPlatform.V),
}
}
return row.InstallerID.V, vppAppID, row.InHouseID.V, nil
@@ -2963,6 +2963,39 @@ WHERE
return res, nil
}
func (ds *Datastore) GetIncludedHostUUIDMapForAppStoreApp(ctx context.Context, vppAppTeamID uint) (map[string]string, error) {
return ds.getIncludedHostUUIDMapForSoftware(ctx, ds.writer(ctx), vppAppTeamID, softwareTypeVPP)
}
func (ds *Datastore) getIncludedHostUUIDMapForSoftware(ctx context.Context, tx sqlx.ExtContext, softwareID uint, swType softwareType) (map[string]string, error) {
filter := fmt.Sprintf(labelScopedFilter, swType)
stmt := fmt.Sprintf(`SELECT
h.uuid AS uuid,
ad.applied_policy_id AS applied_policy_id
FROM
hosts h
JOIN android_devices ad ON ad.enterprise_specific_id = h.uuid
WHERE
EXISTS (%s)
AND platform = 'android'
`, filter)
var queryResults []struct {
UUID string `db:"uuid"`
AppliedPolicyID string `db:"applied_policy_id"`
}
if err := sqlx.SelectContext(ctx, tx, &queryResults, stmt, softwareID, softwareID, softwareID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing hosts included in software scope")
}
res := make(map[string]string, len(queryResults))
for _, result := range queryResults {
res[result.UUID] = result.AppliedPolicyID
}
return res, nil
}
func (ds *Datastore) GetExcludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error) {
return ds.getExcludedHostIDMapForSoftware(ctx, installerID, softwareTypeInstaller)
}
+118 -13
View File
@@ -465,7 +465,7 @@ func (ds *Datastore) SetTeamVPPApps(ctx context.Context, teamID *uint, appFleets
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
for _, toAdd := range toAddApps {
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, toAdd, teamID, vppToken.ID)
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, toAdd, teamID, &vppToken.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "SetTeamVPPApps inserting vpp app into team")
}
@@ -563,9 +563,16 @@ WHERE
}
func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp, teamID *uint) (*fleet.VPPApp, error) {
var vppTokenID *uint
vppToken, err := ds.GetVPPTokenByTeamID(ctx, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam unable to get VPP Token ID")
if !fleet.IsNotFound(err) || app.Platform != fleet.AndroidPlatform {
return nil, ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam unable to get VPP Token ID")
}
}
if vppToken != nil {
vppTokenID = &vppToken.ID
}
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
@@ -580,7 +587,7 @@ func (ds *Datastore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam insertVPPApps transaction")
}
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, app.VPPAppTeam, teamID, vppToken.ID)
vppAppTeamID, err := insertVPPAppTeams(ctx, tx, app.VPPAppTeam, teamID, vppTokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "InsertVPPAppWithTeam insertVPPAppTeams transaction")
}
@@ -711,7 +718,7 @@ ON DUPLICATE KEY UPDATE
return ctxerr.Wrap(ctx, err, "insert VPP apps")
}
func insertVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPAppTeam, teamID *uint, vppTokenID uint) (uint, error) {
func insertVPPAppTeams(ctx context.Context, tx sqlx.ExtContext, appID fleet.VPPAppTeam, teamID *uint, vppTokenID *uint) (uint, error) {
stmt := `
INSERT INTO vpp_apps_teams
(adam_id, global_or_team_id, team_id, platform, self_service, vpp_token_id, install_during_setup)
@@ -732,12 +739,15 @@ ON DUPLICATE KEY UPDATE
}
res, err := tx.ExecContext(ctx, stmt, appID.AdamID, globalOrTmID, teamID, appID.Platform, appID.SelfService, vppTokenID, appID.InstallDuringSetup, appID.InstallDuringSetup)
if IsDuplicate(err) {
err = &existsError{
Identifier: fmt.Sprintf("%s %s self_service: %v", appID.AdamID, appID.Platform, appID.SelfService),
TeamID: teamID,
ResourceType: "VPPAppID",
if err != nil {
if IsDuplicate(err) {
err = &existsError{
Identifier: fmt.Sprintf("%s %s self_service: %v", appID.AdamID, appID.Platform, appID.SelfService),
TeamID: teamID,
ResourceType: "VPPAppID",
}
}
return 0, ctxerr.Wrap(ctx, err, "inserting app store app")
}
var id int64
@@ -780,6 +790,8 @@ func (ds *Datastore) getOrInsertSoftwareTitleForVPPApp(ctx context.Context, tx s
source = "ios_apps"
case fleet.IPadOSPlatform:
source = "ipados_apps"
case fleet.AndroidPlatform:
source = "android_apps"
default:
source = "apps"
}
@@ -792,6 +804,8 @@ func (ds *Datastore) getOrInsertSoftwareTitleForVPPApp(ctx context.Context, tx s
if app.BundleIdentifier != "" {
// match by bundle identifier first, or standard matching if we
// don't have a bundle identifier match
insertStmt = `INSERT INTO software_titles (name, source, bundle_identifier, extension_for) VALUES (?, ?, ?, '')`
insertArgs = append(insertArgs, app.BundleIdentifier)
switch source {
case "ios_apps", "ipados_apps":
selectStmt = `
@@ -801,6 +815,13 @@ func (ds *Datastore) getOrInsertSoftwareTitleForVPPApp(ctx context.Context, tx s
ORDER BY bundle_identifier = ? DESC
LIMIT 1`
selectArgs = []any{app.BundleIdentifier, source, app.Name, source, app.BundleIdentifier}
case "android_apps":
selectStmt = `
SELECT id
FROM software_titles
WHERE application_id = ? AND additional_identifier IS NULL AND source = 'android_apps'`
selectArgs = []any{app.BundleIdentifier}
insertStmt = `INSERT INTO software_titles (name, source, application_id, extension_for) VALUES (?, ?, ?, '')`
default:
selectStmt = `
SELECT id
@@ -808,8 +829,6 @@ func (ds *Datastore) getOrInsertSoftwareTitleForVPPApp(ctx context.Context, tx s
WHERE bundle_identifier = ? AND additional_identifier = 0`
selectArgs = []any{app.BundleIdentifier}
}
insertStmt = `INSERT INTO software_titles (name, source, bundle_identifier, extension_for) VALUES (?, ?, ?, '')`
insertArgs = append(insertArgs, app.BundleIdentifier)
}
titleID, err := ds.optimisticGetOrInsertWithWriter(ctx,
@@ -824,7 +843,7 @@ func (ds *Datastore) getOrInsertSoftwareTitleForVPPApp(ctx context.Context, tx s
},
)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "optimistic get or insert VPP app")
return 0, ctxerr.Wrap(ctx, err, "optimistic get or insert app store app")
}
return titleID, nil
@@ -888,7 +907,7 @@ func (ds *Datastore) GetTitleInfoFromVPPAppsTeamsID(ctx context.Context, vppApps
return &info, nil
}
func (ds *Datastore) GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform fleet.AppleDevicePlatform, teamID *uint) (*fleet.VPPApp, error) {
func (ds *Datastore) GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform fleet.InstallableDevicePlatform, teamID *uint) (*fleet.VPPApp, error) {
stmt := `
SELECT va.adam_id,
va.bundle_identifier,
@@ -1489,6 +1508,11 @@ func (ds *Datastore) DeleteVPPToken(ctx context.Context, tokenID uint) error {
return ctxerr.Wrap(ctx, err, "removing policy automations associated with vpp token")
}
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_apps_teams WHERE vpp_token_id = ?`, tokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "removing vpp apps associated with vpp token")
}
_, err = tx.ExecContext(ctx, `DELETE FROM vpp_tokens WHERE id = ?`, tokenID)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting vpp token")
@@ -2055,3 +2079,84 @@ WHERE
return users, activities, nil
}
func (ds *Datastore) GetAndroidAppsInScopeForHost(ctx context.Context, hostID uint) (applicationIDs []string, err error) {
stmt := `
SELECT
installable_id
FROM (
-- no labels
SELECT
0 AS count_installer_labels,
0 AS count_host_labels,
0 AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM vpp_apps_teams
LEFT JOIN vpp_app_team_labels ON vpp_app_team_labels.vpp_app_team_id = vpp_apps_teams.id
WHERE vpp_app_team_labels.id IS NULL AND vpp_apps_teams.platform = 'android'
UNION
-- include any
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels,
0 AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM
vpp_app_team_labels vatl
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id
LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id
AND lm.host_id = ?
WHERE vatl.exclude = 0 AND vpp_apps_teams.platform = 'android'
GROUP BY installable_id
HAVING
count_installer_labels > 0
AND count_host_labels > 0
UNION
-- exclude any, ignore software that depends on labels created
-- _after_ the label_updated_at timestamp of the host (because
-- we don't have results for that label yet, the host may or may
-- not be a member).
SELECT
COUNT(*) AS count_installer_labels,
COUNT(lm.label_id) AS count_host_labels,
SUM(
CASE WHEN lbl.created_at IS NOT NULL
AND lbl.label_membership_type = 0
AND(
SELECT
label_updated_at FROM hosts
WHERE
id = ?) >= lbl.created_at THEN
1
WHEN lbl.created_at IS NOT NULL
AND lbl.label_membership_type = 1 THEN
1
ELSE
0
END) AS count_host_updated_after_labels,
vpp_apps_teams.adam_id AS installable_id
FROM
vpp_app_team_labels vatl
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.id = vatl.vpp_app_team_id
LEFT OUTER JOIN labels lbl ON lbl.id = vatl.label_id
LEFT OUTER JOIN label_membership lm ON lm.label_id = vatl.label_id
AND lm.host_id = ?
WHERE vatl.exclude = 1 AND vpp_apps_teams.platform = 'android'
GROUP BY installable_id
HAVING
count_installer_labels > 0
AND count_installer_labels = count_host_updated_after_labels
AND count_host_labels = 0) t;
`
err = sqlx.SelectContext(ctx, ds.reader(ctx), &applicationIDs, stmt, hostID, hostID, hostID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get in android apps in scope for host")
}
return applicationIDs, err
}
+1 -1
View File
@@ -481,7 +481,7 @@ func createVPPAppInstallRequest(t *testing.T, ds *Datastore, host *fleet.Host, a
err := ds.InsertHostVPPSoftwareInstall(ctx, host.ID, fleet.VPPAppID{
AdamID: adamID,
Platform: fleet.AppleDevicePlatform(host.Platform),
Platform: fleet.InstallableDevicePlatform(host.Platform),
}, cmdUUID, eventID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
return cmdUUID
+36 -27
View File
@@ -10,6 +10,15 @@ import (
type ContextKey string
type ActivityWebhookPayload struct {
Timestamp time.Time `json:"timestamp"`
ActorFullName *string `json:"actor_full_name"`
ActorID *uint `json:"actor_id"`
ActorEmail *string `json:"actor_email"`
Type string `json:"type"`
Details *json.RawMessage `json:"details"`
}
// ActivityWebhookContextKey is the context key to indicate that the activity webhook has been processed before saving the activity.
const ActivityWebhookContextKey = ContextKey("ActivityWebhook")
@@ -2244,15 +2253,15 @@ func (a ActivityDisabledVPP) Documentation() (activity string, details string, d
}
type ActivityAddedAppStoreApp struct {
SoftwareTitle string `json:"software_title"`
SoftwareTitleId uint `json:"software_title_id"`
AppStoreID string `json:"app_store_id"`
TeamName *string `json:"team_name"`
TeamID *uint `json:"team_id"`
Platform AppleDevicePlatform `json:"platform"`
SelfService bool `json:"self_service"`
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleId uint `json:"software_title_id"`
AppStoreID string `json:"app_store_id"`
TeamName *string `json:"team_name"`
TeamID *uint `json:"team_id"`
Platform InstallableDevicePlatform `json:"platform"`
SelfService bool `json:"self_service"`
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
}
func (a ActivityAddedAppStoreApp) ActivityName() string {
@@ -2291,14 +2300,14 @@ func (a ActivityAddedAppStoreApp) Documentation() (activity string, details stri
}
type ActivityDeletedAppStoreApp struct {
SoftwareTitle string `json:"software_title"`
AppStoreID string `json:"app_store_id"`
TeamName *string `json:"team_name"`
TeamID *uint `json:"team_id"`
Platform AppleDevicePlatform `json:"platform"`
SoftwareIconURL *string `json:"software_icon_url"`
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
SoftwareTitle string `json:"software_title"`
AppStoreID string `json:"app_store_id"`
TeamName *string `json:"team_name"`
TeamID *uint `json:"team_id"`
Platform InstallableDevicePlatform `json:"platform"`
SoftwareIconURL *string `json:"software_icon_url"`
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
}
func (a ActivityDeletedAppStoreApp) ActivityName() string {
@@ -2393,16 +2402,16 @@ func (a ActivityInstalledAppStoreApp) Documentation() (string, string, string) {
}
type ActivityEditedAppStoreApp struct {
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
AppStoreID string `json:"app_store_id"`
TeamName *string `json:"team_name"`
TeamID *uint `json:"team_id"`
Platform AppleDevicePlatform `json:"platform"`
SelfService bool `json:"self_service"`
SoftwareIconURL *string `json:"software_icon_url"`
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
AppStoreID string `json:"app_store_id"`
TeamName *string `json:"team_name"`
TeamID *uint `json:"team_id"`
Platform InstallableDevicePlatform `json:"platform"`
SelfService bool `json:"self_service"`
SoftwareIconURL *string `json:"software_icon_url"`
LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"`
LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"`
}
func (a ActivityEditedAppStoreApp) ActivityName() string {
+5 -1
View File
@@ -2058,7 +2058,7 @@ type Datastore interface {
// GetVPPAppMetadataByAdamIDPlatformTeamID returns the VPP app correspoding to the specified
// ADAM ID, platform within the context of the specified team. It includes the vpp_app_team_id value.
GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform AppleDevicePlatform, teamID *uint) (*VPPApp, error)
GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform InstallableDevicePlatform, teamID *uint) (*VPPApp, error)
// DeleteSoftwareInstaller deletes the software installer corresponding to the id.
DeleteSoftwareInstaller(ctx context.Context, id uint) error
@@ -2067,6 +2067,8 @@ type Datastore interface {
// the provided team.
DeleteVPPAppFromTeam(ctx context.Context, teamID *uint, appID VPPAppID) error
GetAndroidAppsInScopeForHost(ctx context.Context, hostID uint) (applicationIDs []string, err error)
// GetSummaryHostSoftwareInstalls returns the software install summary for
// the given software installer id.
GetSummaryHostSoftwareInstalls(ctx context.Context, installerID uint) (*SoftwareInstallerStatusSummary, error)
@@ -2138,6 +2140,8 @@ type Datastore interface {
// given VPP app, based on label membership.
GetIncludedHostIDMapForVPPApp(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error)
GetIncludedHostUUIDMapForAppStoreApp(ctx context.Context, vppAppTeamID uint) (map[string]string, error)
// GetExcludedHostIDMapForVPPApp gets the set of hosts that are NOT targeted/in scope for the
// given VPP app, based on label membership.
GetExcludedHostIDMapForVPPApp(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error)
+11 -5
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"net/url"
"regexp"
"slices"
"time"
mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
@@ -1042,15 +1043,20 @@ const (
IPadOS
)
type AppleDevicePlatform string
type InstallableDevicePlatform string
const (
MacOSPlatform AppleDevicePlatform = "darwin"
IOSPlatform AppleDevicePlatform = "ios"
IPadOSPlatform AppleDevicePlatform = "ipados"
MacOSPlatform InstallableDevicePlatform = "darwin"
IOSPlatform InstallableDevicePlatform = "ios"
IPadOSPlatform InstallableDevicePlatform = "ipados"
AndroidPlatform InstallableDevicePlatform = "android"
)
var VPPAppsPlatforms = []AppleDevicePlatform{IOSPlatform, IPadOSPlatform, MacOSPlatform}
var VPPAppsPlatforms = []InstallableDevicePlatform{IOSPlatform, IPadOSPlatform, MacOSPlatform, AndroidPlatform}
func (p InstallableDevicePlatform) IsValidInstallableDevicePlatform() bool {
return slices.Contains(VPPAppsPlatforms, p)
}
type AppleDevicesToRefetch struct {
HostID uint `db:"host_id"`
+3 -3
View File
@@ -310,9 +310,9 @@ type PolicySoftwareInstallerData struct {
}
type PolicyVPPData struct {
ID uint `db:"id"`
AdamID string `db:"adam_id"`
Platform AppleDevicePlatform `db:"platform"`
ID uint `db:"id"`
AdamID string `db:"adam_id"`
Platform InstallableDevicePlatform `db:"platform"`
}
type PolicyScriptData struct {
+1 -1
View File
@@ -96,7 +96,7 @@ func (s *SetupExperienceStatusResult) VPPAppID() (*VPPAppID, error) {
return &VPPAppID{
AdamID: *s.VPPAppAdamID,
Platform: AppleDevicePlatform(*s.VPPAppPlatform),
Platform: InstallableDevicePlatform(*s.VPPAppPlatform),
}, nil
}
+6 -6
View File
@@ -642,12 +642,12 @@ type VPPBatchPayload struct {
}
type VPPBatchPayloadWithPlatform struct {
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
Platform AppleDevicePlatform `json:"platform"`
InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated
LabelsExcludeAny []string `json:"labels_exclude_any"`
LabelsIncludeAny []string `json:"labels_include_any"`
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
Platform InstallableDevicePlatform `json:"platform"`
InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated
LabelsExcludeAny []string `json:"labels_exclude_any"`
LabelsIncludeAny []string `json:"labels_include_any"`
// Categories is the list of names of software categories associated with this VPP app.
Categories []string `json:"categories"`
// CategoryIDs is the list of IDs of software categories associated with this VPP app.
+1 -1
View File
@@ -180,7 +180,7 @@ type VPPAppResponse struct {
// AppStoreID is the ADAM ID for this app (set when uploading via batch/gitops).
AppStoreID string `json:"app_store_id" db:"app_store_id"`
// Platform is the platform this title ID corresponds to
Platform AppleDevicePlatform `json:"platform" db:"platform"`
Platform InstallableDevicePlatform `json:"platform" db:"platform"`
//// Custom icon fields (blank if not set)
+14 -14
View File
@@ -46,18 +46,18 @@ type SoftwareTitleIconStore interface {
}
type DetailsForSoftwareIconActivity struct {
SoftwareInstallerID *uint `db:"software_installer_id"`
InHouseAppID *uint `db:"in_house_app_id"`
AdamID *string `db:"adam_id"`
VPPAppTeamID *uint `db:"vpp_app_team_id"`
VPPIconUrl *string `db:"vpp_icon_url"`
SoftwareTitle string `db:"software_title"`
Filename *string `db:"filename"`
TeamName *string `db:"team_name"`
TeamID uint `db:"team_id"`
SelfService bool `db:"self_service"`
SoftwareTitleID uint `db:"software_title_id"`
Platform *AppleDevicePlatform `json:"platform"`
LabelsIncludeAny []ActivitySoftwareLabel `db:"-"`
LabelsExcludeAny []ActivitySoftwareLabel `db:"-"`
SoftwareInstallerID *uint `db:"software_installer_id"`
InHouseAppID *uint `db:"in_house_app_id"`
AdamID *string `db:"adam_id"`
VPPAppTeamID *uint `db:"vpp_app_team_id"`
VPPIconUrl *string `db:"vpp_icon_url"`
SoftwareTitle string `db:"software_title"`
Filename *string `db:"filename"`
TeamName *string `db:"team_name"`
TeamID uint `db:"team_id"`
SelfService bool `db:"self_service"`
SoftwareTitleID uint `db:"software_title_id"`
Platform *InstallableDevicePlatform `json:"platform"`
LabelsIncludeAny []ActivitySoftwareLabel `db:"-"`
LabelsExcludeAny []ActivitySoftwareLabel `db:"-"`
}
+2 -2
View File
@@ -8,8 +8,8 @@ import (
type VPPAppID struct {
// AdamID is a unique identifier assigned to each app in
// the App Store, this value is managed by Apple.
AdamID string `db:"adam_id" json:"app_store_id"`
Platform AppleDevicePlatform `db:"platform" json:"platform"`
AdamID string `db:"adam_id" json:"app_store_id"`
Platform InstallableDevicePlatform `db:"platform" json:"platform"`
}
// VPPAppTeam contains extra metadata injected by fleet
+1
View File
@@ -28,6 +28,7 @@ func TestAllAndroidPackageDependencies(t *testing.T) {
"github.com/fleetdm/fleet/v4/server/service/middleware/log",
"github.com/fleetdm/fleet/v4/server/service/middleware/ratelimit",
"github.com/fleetdm/fleet/v4/server/mdm/android/tests...", // Android functionality moved to main datastore
"github.com/fleetdm/fleet/v4/server/mdm/android/service", // Activities module
).
ShouldNotDependOn(
"github.com/fleetdm/fleet/v4/server/service...",
+24
View File
@@ -33,6 +33,10 @@ type EnterprisesListFunc func(ctx context.Context, serverURL string) ([]*android
type SetAuthenticationSecretFunc func(secret string) error
type EnterprisesApplicationsFunc func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error)
type EnterprisesPoliciesModifyPolicyApplicationsFunc func(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error)
type Client struct {
SignupURLsCreateFunc SignupURLsCreateFunc
SignupURLsCreateFuncInvoked bool
@@ -64,6 +68,12 @@ type Client struct {
SetAuthenticationSecretFunc SetAuthenticationSecretFunc
SetAuthenticationSecretFuncInvoked bool
EnterprisesApplicationsFunc EnterprisesApplicationsFunc
EnterprisesApplicationsFuncInvoked bool
EnterprisesPoliciesModifyPolicyApplicationsFunc EnterprisesPoliciesModifyPolicyApplicationsFunc
EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked bool
mu sync.Mutex
}
@@ -136,3 +146,17 @@ func (p *Client) SetAuthenticationSecret(secret string) error {
p.mu.Unlock()
return p.SetAuthenticationSecretFunc(secret)
}
func (p *Client) EnterprisesApplications(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
p.mu.Lock()
p.EnterprisesApplicationsFuncInvoked = true
p.mu.Unlock()
return p.EnterprisesApplicationsFunc(ctx, enterpriseName, packageName)
}
func (p *Client) EnterprisesPoliciesModifyPolicyApplications(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error) {
p.mu.Lock()
p.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked = true
p.mu.Unlock()
return p.EnterprisesPoliciesModifyPolicyApplicationsFunc(ctx, policyName, appPolicies)
}
+7
View File
@@ -3,6 +3,8 @@ package android
import (
"context"
"net/http"
"google.golang.org/api/androidmanagement/v1"
)
type Service interface {
@@ -18,7 +20,12 @@ type Service interface {
// UnenrollAndroidHost triggers unenrollment (work profile removal) for the given Android host ID.
UnenrollAndroidHost(ctx context.Context, hostID uint) error
EnterprisesApplications(ctx context.Context, enterpriseName, applicationID string) (*androidmanagement.Application, error)
AddAppToAndroidPolicy(ctx context.Context, enterpriseName string, applicationIDs []string, hostUUIDs map[string]string) error
EnableAppReportsOnDefaultPolicy(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)
}
// /////////////////////////////////////////////
@@ -53,6 +53,10 @@ type Client interface {
// SetAuthenticationSecret sets the secret used for authentication.
SetAuthenticationSecret(secret string) error
EnterprisesApplications(ctx context.Context, enterpriseName, packageName string) (*androidmanagement.Application, error)
EnterprisesPoliciesModifyPolicyApplications(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error)
}
type EnterprisesCreateRequest struct {
@@ -5,12 +5,15 @@ import (
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"reflect"
"strings"
"time"
"cloud.google.com/go/pubsub"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/go-json-experiment/json"
kitlog "github.com/go-kit/log"
@@ -68,9 +71,6 @@ func NewGoogleClient(ctx context.Context, logger kitlog.Logger, getenv func(stri
}
func (g *GoogleClient) SignupURLsCreate(ctx context.Context, _, callbackURL string) (*android.SignupDetails, error) {
if g == nil || g.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
signupURL, err := g.mgmt.SignupUrls.Create().ProjectId(g.androidProjectID).CallbackUrl(callbackURL).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("creating signup url: %w", err)
@@ -83,9 +83,6 @@ func (g *GoogleClient) SignupURLsCreate(ctx context.Context, _, callbackURL stri
func (g *GoogleClient) EnterprisesCreate(ctx context.Context, req EnterprisesCreateRequest) (EnterprisesCreateResponse, error) {
res := EnterprisesCreateResponse{}
if g == nil || g.mgmt == nil {
return res, errors.New("android management service not initialized")
}
topicName, err := g.createPubSub(ctx, req.PubSubPushURL)
if err != nil {
@@ -162,8 +159,37 @@ func (g *GoogleClient) createPubSub(ctx context.Context, pushURL string) (string
return topic.String(), nil
}
// generatePolicyFieldMask creates an "update mask": a list of an androidmanagement.Policy's fields that will be updated in
// a given call to EnterprisesPoliciesPatch. We omit `applications` from this list of fields to ensure that apps are only
// updated through calls to EnterprisesPoliciesModifyPolicyApplications.
// See https://developers.google.com/android/management/reference/rest/v1/enterprises.policies/patch#query-parameters
// for more details.
func generatePolicyFieldMask() string {
getJSONFieldName := func(t string) string {
fieldName, _, _ := strings.Cut(t, ",")
return fieldName
}
var p androidmanagement.Policy
t := reflect.TypeOf(p)
var mask []string
for i := range t.NumField() {
f := t.Field(i)
jsonTag, ok := f.Tag.Lookup("json")
// ignore applications because we manage that directly
if n := getJSONFieldName(jsonTag); ok &&
n != "applications" &&
n != "-" && n != "string" && n != "omitempty" {
mask = append(mask, n)
}
}
return strings.Join(mask, ",")
}
var policyFieldMask = generatePolicyFieldMask()
func (g *GoogleClient) EnterprisesPoliciesPatch(ctx context.Context, policyName string, policy *androidmanagement.Policy) (*androidmanagement.Policy, error) {
ret, err := g.mgmt.Enterprises.Policies.Patch(policyName, policy).Context(ctx).Do()
ret, err := g.mgmt.Enterprises.Policies.Patch(policyName, policy).Context(ctx).UpdateMask(policyFieldMask).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android policy not modified", "policy_name", policyName)
@@ -187,9 +213,6 @@ func (g *GoogleClient) EnterprisesDevicesPatch(ctx context.Context, deviceName s
}
func (g *GoogleClient) EnterprisesDevicesGet(ctx context.Context, deviceName string) (*androidmanagement.Device, error) {
if g == nil || g.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
ret, err := g.mgmt.Enterprises.Devices.Get(deviceName).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("getting device %s: %w", deviceName, err)
@@ -198,9 +221,6 @@ func (g *GoogleClient) EnterprisesDevicesGet(ctx context.Context, deviceName str
}
func (g *GoogleClient) EnterprisesDevicesDelete(ctx context.Context, deviceName string) error {
if g == nil || g.mgmt == nil {
return errors.New("android management service not initialized")
}
_, err := g.mgmt.Enterprises.Devices.Delete(deviceName).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
@@ -214,9 +234,6 @@ func (g *GoogleClient) EnterprisesDevicesDelete(ctx context.Context, deviceName
func (g *GoogleClient) EnterprisesEnrollmentTokensCreate(ctx context.Context, enterpriseName string, token *androidmanagement.EnrollmentToken,
) (*androidmanagement.EnrollmentToken, error) {
if g == nil || g.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
token, err := g.mgmt.Enterprises.EnrollmentTokens.Create(enterpriseName, token).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("creating enrollment token: %w", err)
@@ -225,10 +242,6 @@ func (g *GoogleClient) EnterprisesEnrollmentTokensCreate(ctx context.Context, en
}
func (g *GoogleClient) EnterpriseDelete(ctx context.Context, enterpriseName string) error {
if g == nil || g.mgmt == nil {
return errors.New("android management service not initialized")
}
// To find out the enterprise's PubSub topic, we need to get the enterprise first.
// We can also pull the topic from the DB, but this way is more reliable.
enterprise, err := g.mgmt.Enterprises.Get(enterpriseName).Context(ctx).Do()
@@ -276,9 +289,6 @@ func (g *GoogleClient) EnterpriseDelete(ctx context.Context, enterpriseName stri
}
func (g *GoogleClient) EnterprisesList(ctx context.Context, serverURL string) ([]*androidmanagement.Enterprise, error) {
if g == nil || g.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
resp, err := g.mgmt.Enterprises.List().ProjectId(g.androidProjectID).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("listing enterprises: %w", err)
@@ -298,3 +308,51 @@ func getLastPart(ctx context.Context, name string) (string, error) {
}
return nameParts[len(nameParts)-1], nil
}
type appNotFoundError struct{}
var _ fleet.NotFoundError = (*appNotFoundError)(nil)
func (p appNotFoundError) Error() string {
return "Couldnt add software. The application ID isnt available in Play Store. Please find ID on the Play Store and try again."
}
func (p appNotFoundError) IsNotFound() bool {
return true
}
func (g *GoogleClient) EnterprisesApplications(ctx context.Context, enterpriseName, packageName string) (*androidmanagement.Application, error) {
path := fmt.Sprintf("%s/applications/%s", enterpriseName, packageName)
app, err := g.mgmt.Enterprises.Applications.Get(path).Context(ctx).Do()
if err != nil {
var gapiErr *googleapi.Error
if errors.As(err, &gapiErr) {
if gapiErr.Code == http.StatusNotFound {
return nil, ctxerr.Wrap(ctx, appNotFoundError{})
}
}
return nil, fmt.Errorf("getting application %s: %w", packageName, err)
}
return app, nil
}
func (g *GoogleClient) EnterprisesPoliciesModifyPolicyApplications(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error) {
var changes []*androidmanagement.ApplicationPolicyChange
for _, p := range appPolicies {
changes = append(changes, &androidmanagement.ApplicationPolicyChange{
Application: p,
})
}
req := androidmanagement.ModifyPolicyApplicationsRequest{
Changes: changes,
}
ret, err := g.mgmt.Enterprises.Policies.ModifyPolicyApplications(policyName, &req).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "modifying application policy %s", policyName)
}
return ret.Policy, nil
}
@@ -0,0 +1,15 @@
package androidmgmt
import (
"testing"
"github.com/tj/assert"
)
func TestGeneratePolicyFieldMask(t *testing.T) {
const expectedMask = `accountTypesWithManagementDisabled,addUserDisabled,adjustVolumeDisabled,advancedSecurityOverrides,alwaysOnVpnPackage,androidDevicePolicyTracks,appAutoUpdatePolicy,appFunctions,assistContentPolicy,autoDateAndTimeZone,autoTimeRequired,blockApplicationsEnabled,bluetoothConfigDisabled,bluetoothContactSharingDisabled,bluetoothDisabled,cameraAccess,cameraDisabled,cellBroadcastsConfigDisabled,choosePrivateKeyRules,complianceRules,createWindowsDisabled,credentialProviderPolicyDefault,credentialsConfigDisabled,crossProfilePolicies,dataRoamingDisabled,debuggingFeaturesAllowed,defaultPermissionPolicy,deviceConnectivityManagement,deviceOwnerLockScreenInfo,deviceRadioState,displaySettings,encryptionPolicy,ensureVerifyAppsEnabled,enterpriseDisplayNameVisibility,factoryResetDisabled,frpAdminEmails,funDisabled,installAppsDisabled,installUnknownSourcesAllowed,keyguardDisabled,keyguardDisabledFeatures,kioskCustomLauncherEnabled,kioskCustomization,locationMode,longSupportMessage,maximumTimeToLock,microphoneAccess,minimumApiLevel,mobileNetworksConfigDisabled,modifyAccountsDisabled,mountPhysicalMediaDisabled,name,networkEscapeHatchEnabled,networkResetDisabled,oncCertificateProviders,openNetworkConfiguration,outgoingBeamDisabled,outgoingCallsDisabled,passwordPolicies,passwordRequirements,permissionGrants,permittedAccessibilityServices,permittedInputMethods,persistentPreferredActivities,personalUsagePolicies,playStoreMode,policyEnforcementRules,preferentialNetworkService,printingPolicy,privateKeySelectionEnabled,recommendedGlobalProxy,removeUserDisabled,safeBootDisabled,screenCaptureDisabled,setUserIconDisabled,setWallpaperDisabled,setupActions,shareLocationDisabled,shortSupportMessage,skipFirstUseHintsEnabled,smsDisabled,statusBarDisabled,statusReportingSettings,stayOnPluggedModes,systemUpdate,tetheringConfigDisabled,uninstallAppsDisabled,unmuteMicrophoneDisabled,usageLog,usbFileTransferDisabled,usbMassStorageEnabled,version,vpnConfigDisabled,wifiConfigDisabled,wifiConfigsLockdownEnabled,wipeDataFlags,workAccountSetupConfig`
mask := generatePolicyFieldMask()
assert.NotContains(t, mask, "omitempty")
assert.NotContains(t, mask, "applications")
assert.Equal(t, mask, expectedMask)
}
@@ -10,6 +10,7 @@ import (
"os"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/go-json-experiment/json"
kitlog "github.com/go-kit/log"
@@ -69,9 +70,6 @@ func (p *ProxyClient) SetAuthenticationSecret(secret string) error {
// SignupURLsCreate hits the unauthenticated endpoint of the proxy. If a record already exists for this serverURL,
// then the proxy will return a conflict error.
func (p *ProxyClient) SignupURLsCreate(ctx context.Context, serverURL, callbackURL string) (*android.SignupDetails, error) {
if p == nil || p.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
call := p.mgmt.SignupUrls.Create().CallbackUrl(callbackURL).Context(ctx)
call.Header().Set("Origin", serverURL)
signupURL, err := call.Do()
@@ -92,10 +90,6 @@ func (p *ProxyClient) SignupURLsCreate(ctx context.Context, serverURL, callbackU
// The reason is that we are passing additional information such as license key, pubSubURL, etc. Because of that,
// we use a separate HTTP client in this method.
func (p *ProxyClient) EnterprisesCreate(ctx context.Context, req EnterprisesCreateRequest) (EnterprisesCreateResponse, error) {
if p == nil || p.mgmt == nil {
return EnterprisesCreateResponse{}, errors.New("android management service not initialized")
}
type proxyEnterprise struct {
FleetLicenseKey string `json:"fleetLicenseKey"`
PubSubPushURL string `json:"pubsubPushUrl"`
@@ -155,7 +149,7 @@ func (p *ProxyClient) EnterprisesCreate(ctx context.Context, req EnterprisesCrea
}
func (p *ProxyClient) EnterprisesPoliciesPatch(ctx context.Context, policyName string, policy *androidmanagement.Policy) (*androidmanagement.Policy, error) {
call := p.mgmt.Enterprises.Policies.Patch(policyName, policy).Context(ctx)
call := p.mgmt.Enterprises.Policies.Patch(policyName, policy).Context(ctx).UpdateMask(policyFieldMask)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
ret, err := call.Do()
switch {
@@ -183,9 +177,6 @@ func (p *ProxyClient) EnterprisesDevicesPatch(ctx context.Context, deviceName st
}
func (p *ProxyClient) EnterprisesDevicesGet(ctx context.Context, deviceName string) (*androidmanagement.Device, error) {
if p == nil || p.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
call := p.mgmt.Enterprises.Devices.Get(deviceName).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
ret, err := call.Do()
@@ -196,9 +187,6 @@ func (p *ProxyClient) EnterprisesDevicesGet(ctx context.Context, deviceName stri
}
func (p *ProxyClient) EnterprisesDevicesDelete(ctx context.Context, deviceName string) error {
if p == nil || p.mgmt == nil {
return errors.New("android management service not initialized")
}
call := p.mgmt.Enterprises.Devices.Delete(deviceName).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
_, err := call.Do()
@@ -214,9 +202,7 @@ func (p *ProxyClient) EnterprisesDevicesDelete(ctx context.Context, deviceName s
func (p *ProxyClient) EnterprisesEnrollmentTokensCreate(ctx context.Context, enterpriseName string,
token *androidmanagement.EnrollmentToken) (*androidmanagement.EnrollmentToken, error) {
if p == nil || p.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
call := p.mgmt.Enterprises.EnrollmentTokens.Create(enterpriseName, token).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
token, err := call.Do()
@@ -227,10 +213,6 @@ func (p *ProxyClient) EnterprisesEnrollmentTokensCreate(ctx context.Context, ent
}
func (p *ProxyClient) EnterpriseDelete(ctx context.Context, enterpriseName string) error {
if p == nil || p.mgmt == nil {
return errors.New("android management service not initialized")
}
call := p.mgmt.Enterprises.Delete(enterpriseName).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
_, err := call.Do()
@@ -246,9 +228,6 @@ func (p *ProxyClient) EnterpriseDelete(ctx context.Context, enterpriseName strin
}
func (p *ProxyClient) EnterprisesList(ctx context.Context, serverURL string) ([]*androidmanagement.Enterprise, error) {
if p == nil || p.mgmt == nil {
return nil, errors.New("android management service not initialized")
}
call := p.mgmt.Enterprises.List().Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
call.Header().Set("Origin", serverURL)
@@ -281,3 +260,47 @@ func isErrorCode(err error, code int) bool {
ok := errors.As(err, &ae)
return ok && ae.Code == code
}
func (p *ProxyClient) EnterprisesApplications(ctx context.Context, enterpriseName, packageName string) (*androidmanagement.Application, error) {
path := fmt.Sprintf("%s/applications/%s", enterpriseName, packageName)
call := p.mgmt.Enterprises.Applications.Get(path).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
app, err := call.Do()
if err != nil {
var gapiErr *googleapi.Error
if errors.As(err, &gapiErr) {
if gapiErr.Code == http.StatusNotFound {
return nil, ctxerr.Wrap(ctx, appNotFoundError{})
}
}
return nil, fmt.Errorf("getting application %s: %w", packageName, err)
}
return app, nil
}
func (p *ProxyClient) EnterprisesPoliciesModifyPolicyApplications(ctx context.Context, policyName string, appPolicies []*androidmanagement.ApplicationPolicy) (*androidmanagement.Policy, error) {
var changes []*androidmanagement.ApplicationPolicyChange
for _, p := range appPolicies {
changes = append(changes, &androidmanagement.ApplicationPolicyChange{
Application: p,
})
}
req := androidmanagement.ModifyPolicyApplicationsRequest{
Changes: changes,
}
call := p.mgmt.Enterprises.Policies.ModifyPolicyApplications(policyName, &req).Context(ctx)
call.Header().Set("Authorization", "Bearer "+p.fleetServerSecret)
ret, err := call.Do()
switch {
case googleapi.IsNotModified(err):
p.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "modifying application policy %s", policyName)
}
return ret.Policy, nil
}
+26 -29
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
@@ -14,10 +15,10 @@ import (
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
ds_mock "github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/googleapi"
@@ -28,8 +29,8 @@ func TestEnterprisesAuth(t *testing.T) {
androidAPIClient.InitCommonMocks()
logger := kitlog.NewLogfmtLogger(os.Stdout)
fleetDS := InitCommonDSMocks()
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
testCases := []struct {
@@ -126,9 +127,9 @@ func TestEnterpriseSignupMissingPrivateKey(t *testing.T) {
androidAPIClient.InitCommonMocks()
logger := kitlog.NewLogfmtLogger(os.Stdout)
fleetDS := InitCommonDSMocks()
fleetSvc := mockService{}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
@@ -210,6 +211,12 @@ func InitCommonDSMocks() *AndroidMockDS {
ds.Store.BulkSetAndroidHostsUnenrolledFunc = func(ctx context.Context) error {
return nil
}
ds.Store.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
return nil
}
ds.Store.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
return &ds
}
@@ -222,16 +229,6 @@ type notFoundError struct{}
func (e *notFoundError) Error() string { return "not found" }
func (e *notFoundError) IsNotFound() bool { return true }
type mockService struct {
mock.Mock
fleet.Service
}
// NewActivity mocks the fleet.Service method.
func (m *mockService) NewActivity(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error {
return nil
}
func TestGetEnterprise(t *testing.T) {
logger := kitlog.NewLogfmtLogger(os.Stdout)
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
@@ -242,8 +239,8 @@ func TestGetEnterprise(t *testing.T) {
androidAPIClient.InitCommonMocks()
fleetDS := InitCommonDSMocks()
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -279,8 +276,8 @@ func TestGetEnterprise(t *testing.T) {
}, nil
}
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -329,8 +326,8 @@ func TestGetEnterprise(t *testing.T) {
}, nil
}
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -371,8 +368,8 @@ func TestGetEnterprise(t *testing.T) {
}
}
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -413,8 +410,8 @@ func TestGetEnterprise(t *testing.T) {
return []*androidmanagement.Enterprise{}, nil
}
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -481,8 +478,8 @@ func TestGetEnterprise(t *testing.T) {
}, nil
}
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -513,8 +510,8 @@ func TestGetEnterprise(t *testing.T) {
return nil, &notFoundError{}
}
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, &fleetSvc, "test-private-key", &fleetDS.DataStore)
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule)
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
+20 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/go-json-experiment/json"
"github.com/go-kit/log/level"
"golang.org/x/text/cases"
@@ -152,7 +153,7 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
// Emit system activity: mdm_unenrolled. For Android BYOD, InstalledFromDEP is always false.
// Use the computed display name from the device payload as lite host may not include it.
displayName := svc.getComputerName(&device)
_ = svc.fleetSvc.NewActivity(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
_ = svc.activityModule.NewActivity(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
HostSerial: "",
HostDisplayName: displayName,
InstalledFromDEP: false,
@@ -268,7 +269,7 @@ func (svc *Service) handlePubSubEnrollment(ctx context.Context, token string, ra
return ctxerr.Wrap(ctx, err, "set android host unenrolled on DELETED state (ENROLLMENT)")
}
displayName := svc.getComputerName(&device)
_ = svc.fleetSvc.NewActivity(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
_ = svc.activityModule.NewActivity(ctx, nil, fleet.ActivityTypeMDMUnenrolled{
HostSerial: "",
HostDisplayName: displayName,
InstalledFromDEP: false,
@@ -292,6 +293,9 @@ func (svc *Service) enrollHost(ctx context.Context, device *androidmanagement.De
return err
}
// Enqueue a job to send any necessary self-service software.
// Like Martin said below, this should properly be part of a device lifecycle action.
// Device may already be present in Fleet if device user removed the MDM profile and then re-enrolled
host, err := svc.getExistingHost(ctx, device)
if err != nil {
@@ -306,7 +310,7 @@ func (svc *Service) enrollHost(ctx context.Context, device *androidmanagement.De
var enrollmentTokenRequest enrollmentTokenRequest
err = json.Unmarshal([]byte(device.EnrollmentTokenData), &enrollmentTokenRequest)
if err != nil {
return ctxerr.Wrap(ctx, err, "unmarshilling enrollment token data")
return ctxerr.Wrap(ctx, err, "unmarshalling enrollment token data")
}
if host != nil {
@@ -453,8 +457,9 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
DeviceID: deviceID,
},
}
policy := ptr.String(fmt.Sprint(defaultAndroidPolicyID))
if device.AppliedPolicyName != "" {
policy, err := svc.getPolicyID(ctx, device)
policy, err = svc.getPolicyID(ctx, device)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting Android policy ID")
}
@@ -469,7 +474,7 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
host.Device.LastPolicySyncTime = ptr.Time(policySyncTime)
}
host.SetNodeKey(device.HardwareInfo.EnterpriseSpecificId)
_, err = svc.ds.NewAndroidHost(ctx, host)
fleetHost, err := svc.ds.NewAndroidHost(ctx, host)
if err != nil {
return ctxerr.Wrap(ctx, err, "enrolling Android host")
}
@@ -482,6 +487,16 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
}
}
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get android enterprise")
}
err = worker.QueueMakeAndroidAppsAvailableForHostJob(ctx, svc.fleetDS, svc.logger, device.HardwareInfo.EnterpriseSpecificId, fleetHost.Host.ID, enterprise.Name(), *policy)
if err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing make android apps available for host job")
}
return nil
}
+5 -4
View File
@@ -15,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
@@ -26,8 +27,8 @@ func createAndroidService(t *testing.T) (android.Service, *AndroidMockDS) {
androidAPIClient.InitCommonMocks()
logger := kitlog.NewLogfmtLogger(os.Stdout)
mockDS := InitCommonDSMocks()
fleetSvc := mockService{}
svc, err := NewServiceWithClient(logger, mockDS, &androidAPIClient, &fleetSvc, "test-private-key", &mockDS.DataStore)
activityModule := activities.NewActivityModule(mockDS, logger)
svc, err := NewServiceWithClient(logger, mockDS, &androidAPIClient, "test-private-key", &mockDS.DataStore, activityModule)
require.NoError(t, err)
return svc, mockDS
@@ -142,7 +143,7 @@ func TestPubSubEnrollment(t *testing.T) {
}
mockDS.NewAndroidHostFunc = func(ctx context.Context, host *fleet.AndroidHost) (*fleet.AndroidHost, error) {
return nil, nil // We do not care about return value here
return &fleet.AndroidHost{Host: &fleet.Host{}}, nil
}
enrollmentToken := enrollmentTokenRequest{
@@ -171,7 +172,7 @@ func TestPubSubEnrollment(t *testing.T) {
}
mockDS.NewAndroidHostFunc = func(ctx context.Context, host *fleet.AndroidHost) (*fleet.AndroidHost, error) {
return nil, nil // We do not care about return value here
return &fleet.AndroidHost{Host: &fleet.Host{}}, nil
}
mockDS.AssociateHostMDMIdPAccountFunc = func(ctx context.Context, hostUUID, accountUUID string) error {
return nil
+120 -11
View File
@@ -21,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"google.golang.org/api/androidmanagement/v1"
@@ -43,7 +44,7 @@ type Service struct {
ds fleet.AndroidDatastore
fleetDS fleet.Datastore
androidAPIClient androidmgmt.Client
fleetSvc fleet.Service
activityModule activities.ActivityModule
serverPrivateKey string
// SignupSSEInterval can be overwritten in tests.
@@ -56,38 +57,50 @@ func NewService(
ctx context.Context,
logger kitlog.Logger,
ds fleet.AndroidDatastore,
fleetSvc fleet.Service,
licenseKey string,
serverPrivateKey string,
fleetDS fleet.Datastore,
activityModule activities.ActivityModule,
) (android.Service, error) {
client := newAMAPIClient(ctx, logger, licenseKey)
return NewServiceWithClient(logger, ds, client, fleetSvc, serverPrivateKey, fleetDS)
return NewServiceWithClient(logger, ds, client, serverPrivateKey, fleetDS, activityModule)
}
func NewServiceWithClient(
logger kitlog.Logger,
ds fleet.AndroidDatastore,
client androidmgmt.Client,
fleetSvc fleet.Service,
serverPrivateKey string,
fleetDS fleet.Datastore,
activityModule activities.ActivityModule,
) (android.Service, error) {
authorizer, err := authz.NewAuthorizer()
if err != nil {
return nil, fmt.Errorf("new authorizer: %w", err)
}
return &Service{
svc := &Service{
logger: logger,
authz: authorizer,
ds: ds,
androidAPIClient: client,
fleetSvc: fleetSvc,
serverPrivateKey: serverPrivateKey,
SignupSSEInterval: DefaultSignupSSEInterval,
fleetDS: fleetDS,
}, nil
activityModule: activityModule,
}
// OK to use background context here because this function is only called during server bootstrap
// Setting the secret here ensures that we don't have to configure it in lots of different places
// when using the proxy client.
ctx := context.Background()
secret, err := svc.getClientAuthenticationSecret(ctx)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting client authentication secret")
}
_ = svc.androidAPIClient.SetAuthenticationSecret(secret)
return svc, nil
}
func newAMAPIClient(ctx context.Context, logger kitlog.Logger, licenseKey string) androidmgmt.Client {
@@ -358,7 +371,7 @@ func (svc *Service) EnterpriseSignupCallback(ctx context.Context, signupToken st
return ctxerr.Wrap(ctx, err, "getting user")
}
if err = svc.fleetSvc.NewActivity(ctx, user, fleet.ActivityTypeEnabledAndroidMDM{}); err != nil {
if err = svc.activityModule.NewActivity(ctx, user, fleet.ActivityTypeEnabledAndroidMDM{}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for enabled Android MDM")
}
@@ -445,7 +458,7 @@ func (svc *Service) DeleteEnterprise(ctx context.Context) error {
return ctxerr.Wrap(ctx, err, "bulk set android hosts as unenrolled")
}
if err = svc.fleetSvc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDisabledAndroidMDM{}); err != nil {
if err = svc.activityModule.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeDisabledAndroidMDM{}); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for disabled Android MDM")
}
@@ -804,7 +817,7 @@ func unenrollAndroidHostEndpoint(ctx context.Context, request interface{}, svc a
// The actual MDM status flip to Off is performed when Pub/Sub sends DELETED for the device.
func (svc *Service) UnenrollAndroidHost(ctx context.Context, hostID uint) error {
// Load host and authorize based on team
h, err := svc.fleetSvc.GetHostLite(ctx, hostID)
h, err := svc.fleetDS.HostLite(ctx, hostID)
if err != nil {
return err
}
@@ -841,7 +854,7 @@ func (svc *Service) UnenrollAndroidHost(ctx context.Context, hostID uint) error
// Emit activity: admin told Fleet to unenroll
displayName := fleet.HostDisplayName(h.ComputerName, h.Hostname, h.HardwareModel, h.HardwareSerial)
if err := svc.fleetSvc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeMDMUnenrolled{
if err := svc.activityModule.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeMDMUnenrolled{
HostSerial: h.HardwareSerial,
HostDisplayName: displayName,
InstalledFromDEP: false,
@@ -852,6 +865,33 @@ func (svc *Service) UnenrollAndroidHost(ctx context.Context, hostID uint) error
return nil
}
func (svc *Service) EnterprisesApplications(ctx context.Context, enterpriseName, applicationID string) (*androidmanagement.Application, error) {
return svc.androidAPIClient.EnterprisesApplications(ctx, enterpriseName, applicationID)
}
func (svc *Service) AddAppToAndroidPolicy(ctx context.Context, enterpriseName string, applicationIDs []string, hostUUIDs map[string]string) error {
var appPolicies []*androidmanagement.ApplicationPolicy
for _, a := range applicationIDs {
appPolicies = append(appPolicies, &androidmanagement.ApplicationPolicy{
PackageName: a,
InstallType: "AVAILABLE",
})
}
var errs []error
for uuid, policyID := range hostUUIDs {
policyName := fmt.Sprintf("%s/policies/%s", enterpriseName, policyID)
_, err := svc.androidAPIClient.EnterprisesPoliciesModifyPolicyApplications(ctx, policyName, appPolicies)
if err != nil {
errs = append(errs, ctxerr.Wrapf(ctx, err, "google api: modify policy applications for host %s", uuid))
}
}
return errors.Join(errs...)
}
func (svc *Service) EnableAppReportsOnDefaultPolicy(ctx context.Context) error {
enterprise, err := svc.ds.GetEnterprise(ctx)
if err != nil {
@@ -890,3 +930,72 @@ func (svc *Service) EnableAppReportsOnDefaultPolicy(ctx context.Context) error {
}
return nil
}
func (svc *Service) PatchDevice(ctx context.Context, policyID, deviceName string, device *androidmanagement.Device) (skip bool, apiErr error) {
deviceRequest, err := newAndroidDeviceRequest(policyID, deviceName, device)
if err != nil {
return false, ctxerr.Wrapf(ctx, err, "prepare device request %s", deviceName)
}
applied, apiErr := svc.androidAPIClient.EnterprisesDevicesPatch(ctx, deviceName, device)
if apiErr != nil {
var gerr *googleapi.Error
if errors.As(apiErr, &gerr) {
deviceRequest.StatusCode = gerr.Code
}
deviceRequest.ErrorDetails.V = apiErr.Error()
deviceRequest.ErrorDetails.Valid = true
if skip = androidmgmt.IsNotModifiedError(apiErr); skip {
apiErr = nil
}
} else {
deviceRequest.StatusCode = http.StatusOK
deviceRequest.AppliedPolicyVersion.V = applied.AppliedPolicyVersion
deviceRequest.AppliedPolicyVersion.Valid = true
}
if err := svc.fleetDS.NewAndroidPolicyRequest(ctx, deviceRequest); err != nil {
return false, ctxerr.Wrap(ctx, err, "save android device request")
}
return skip, nil
}
func (svc *Service) PatchPolicy(ctx context.Context, policyID, policyName string,
policy *androidmanagement.Policy, metadata map[string]string,
) (skip bool, err error) {
policyRequest, err := newAndroidPolicyRequest(policyID, policyName, policy, metadata)
if err != nil {
return false, ctxerr.Wrapf(ctx, err, "prepare policy request %s", policyName)
}
applied, apiErr := svc.androidAPIClient.EnterprisesPoliciesPatch(ctx, policyName, policy)
if apiErr != nil {
var gerr *googleapi.Error
if errors.As(apiErr, &gerr) {
policyRequest.StatusCode = gerr.Code
}
policyRequest.ErrorDetails.V = apiErr.Error()
policyRequest.ErrorDetails.Valid = true
// Note that from my tests, the "not modified" error is not reliable, the
// AMAPI happily returned 200 even if the policy was the same (as
// confirmed by the same version number being returned), so we do check
// for this error, but do not build critical logic on top of it.
//
// Tests do show that the version number is properly incremented when the
// policy changes, though.
if skip = androidmgmt.IsNotModifiedError(apiErr); skip {
apiErr = nil
}
} else {
policyRequest.StatusCode = http.StatusOK
policyRequest.PolicyVersion.V = applied.Version
policyRequest.PolicyVersion.Valid = true
}
if err := svc.fleetDS.NewAndroidPolicyRequest(ctx, policyRequest); err != nil {
return false, ctxerr.Wrap(ctx, err, "save android policy request")
}
return skip, nil
}
@@ -59,10 +59,8 @@ func (s *enterpriseTestSuite) TestEnterprise() {
assert.Equal(s.T(), tests.EnterpriseSignupURL, signupResp.Url)
s.T().Logf("callbackURL: %s", s.ProxyCallbackURL)
s.FleetSvc.On("NewActivity", mock.Anything, mock.Anything, mock.AnythingOfType("fleet.ActivityTypeEnabledAndroidMDM")).Return(nil)
const enterpriseToken = "enterpriseToken"
res := s.Do("GET", s.ProxyCallbackURL, nil, http.StatusOK, "enterpriseToken", enterpriseToken)
s.FleetSvc.AssertNumberOfCalls(s.T(), "NewActivity", 1)
body, err := io.ReadAll(res.Body)
require.NoError(s.T(), err)
assert.Equal(s.T(), "text/html; charset=UTF-8", res.Header.Get("Content-Type"))
@@ -82,9 +80,7 @@ func (s *enterpriseTestSuite) TestEnterprise() {
assert.Equal(s.T(), tests.EnterpriseID, resp.EnterpriseID)
// Delete enterprise and make sure we can't find it.
s.FleetSvc.On("NewActivity", mock.Anything, mock.Anything, mock.AnythingOfType("fleet.ActivityTypeDisabledAndroidMDM")).Return(nil)
s.Do("DELETE", "/api/v1/fleet/android_enterprise", nil, http.StatusOK)
s.FleetSvc.AssertNumberOfCalls(s.T(), "NewActivity", 2)
// Reset LIST mock to empty after deletion
s.AndroidAPIClient.EnterprisesListFunc = func(_ context.Context, _ string) ([]*androidmanagement.Enterprise, error) {
+7 -1
View File
@@ -7,6 +7,7 @@ import (
"os"
"sync"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
@@ -20,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
"github.com/fleetdm/fleet/v4/server/service/middleware/log"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kithttp "github.com/go-kit/kit/transport/http"
kitlog "github.com/go-kit/log"
"github.com/gorilla/mux"
@@ -110,7 +112,8 @@ func (ts *WithServer) SetupSuite(t *testing.T, dbName string) {
ts.createCommonProxyMocks(t)
logger := kitlog.NewLogfmtLogger(os.Stdout)
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, &ts.FleetSvc, "test-private-key", ts.DS.Datastore)
activityModule := activities.NewActivityModule(&ts.DS.DataStore, logger)
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, "test-private-key", ts.DS.Datastore, activityModule)
require.NoError(t, err)
ts.Svc = svc
@@ -152,6 +155,9 @@ func (ts *WithServer) CreateCommonDSMocks() {
ts.DS.BulkSetAndroidHostsUnenrolledFunc = func(ctx context.Context) error {
return nil
}
ts.DS.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error {
return nil
}
}
func (ts *WithServer) createCommonProxyMocks(t *testing.T) {
+26 -2
View File
@@ -1303,12 +1303,14 @@ type MapAdamIDsPendingInstallFunc func(ctx context.Context, hostID uint) (map[st
type GetTitleInfoFromVPPAppsTeamsIDFunc func(ctx context.Context, vppAppsTeamsID uint) (*fleet.PolicySoftwareTitle, error)
type GetVPPAppMetadataByAdamIDPlatformTeamIDFunc func(ctx context.Context, adamID string, platform fleet.AppleDevicePlatform, teamID *uint) (*fleet.VPPApp, error)
type GetVPPAppMetadataByAdamIDPlatformTeamIDFunc func(ctx context.Context, adamID string, platform fleet.InstallableDevicePlatform, teamID *uint) (*fleet.VPPApp, error)
type DeleteSoftwareInstallerFunc func(ctx context.Context, id uint) error
type DeleteVPPAppFromTeamFunc func(ctx context.Context, teamID *uint, appID fleet.VPPAppID) error
type GetAndroidAppsInScopeForHostFunc func(ctx context.Context, hostID uint) (applicationIDs []string, err error)
type GetSummaryHostSoftwareInstallsFunc func(ctx context.Context, installerID uint) (*fleet.SoftwareInstallerStatusSummary, error)
type GetSummaryHostVPPAppInstallsFunc func(ctx context.Context, teamID *uint, appID fleet.VPPAppID) (*fleet.VPPAppStatusSummary, error)
@@ -1373,6 +1375,8 @@ type GetVPPTokenByLocationFunc func(ctx context.Context, loc string) (*fleet.VPP
type GetIncludedHostIDMapForVPPAppFunc func(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error)
type GetIncludedHostUUIDMapForAppStoreAppFunc func(ctx context.Context, vppAppTeamID uint) (map[string]string, error)
type GetExcludedHostIDMapForVPPAppFunc func(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error)
type ClearVPPAppAutoInstallPolicyStatusForHostsFunc func(ctx context.Context, vppAppTeamID uint, hostIDs []uint) error
@@ -3535,6 +3539,9 @@ type DataStore struct {
DeleteVPPAppFromTeamFunc DeleteVPPAppFromTeamFunc
DeleteVPPAppFromTeamFuncInvoked bool
GetAndroidAppsInScopeForHostFunc GetAndroidAppsInScopeForHostFunc
GetAndroidAppsInScopeForHostFuncInvoked bool
GetSummaryHostSoftwareInstallsFunc GetSummaryHostSoftwareInstallsFunc
GetSummaryHostSoftwareInstallsFuncInvoked bool
@@ -3631,6 +3638,9 @@ type DataStore struct {
GetIncludedHostIDMapForVPPAppFunc GetIncludedHostIDMapForVPPAppFunc
GetIncludedHostIDMapForVPPAppFuncInvoked bool
GetIncludedHostUUIDMapForAppStoreAppFunc GetIncludedHostUUIDMapForAppStoreAppFunc
GetIncludedHostUUIDMapForAppStoreAppFuncInvoked bool
GetExcludedHostIDMapForVPPAppFunc GetExcludedHostIDMapForVPPAppFunc
GetExcludedHostIDMapForVPPAppFuncInvoked bool
@@ -8462,7 +8472,7 @@ func (s *DataStore) GetTitleInfoFromVPPAppsTeamsID(ctx context.Context, vppAppsT
return s.GetTitleInfoFromVPPAppsTeamsIDFunc(ctx, vppAppsTeamsID)
}
func (s *DataStore) GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform fleet.AppleDevicePlatform, teamID *uint) (*fleet.VPPApp, error) {
func (s *DataStore) GetVPPAppMetadataByAdamIDPlatformTeamID(ctx context.Context, adamID string, platform fleet.InstallableDevicePlatform, teamID *uint) (*fleet.VPPApp, error) {
s.mu.Lock()
s.GetVPPAppMetadataByAdamIDPlatformTeamIDFuncInvoked = true
s.mu.Unlock()
@@ -8483,6 +8493,13 @@ func (s *DataStore) DeleteVPPAppFromTeam(ctx context.Context, teamID *uint, appI
return s.DeleteVPPAppFromTeamFunc(ctx, teamID, appID)
}
func (s *DataStore) GetAndroidAppsInScopeForHost(ctx context.Context, hostID uint) (applicationIDs []string, err error) {
s.mu.Lock()
s.GetAndroidAppsInScopeForHostFuncInvoked = true
s.mu.Unlock()
return s.GetAndroidAppsInScopeForHostFunc(ctx, hostID)
}
func (s *DataStore) GetSummaryHostSoftwareInstalls(ctx context.Context, installerID uint) (*fleet.SoftwareInstallerStatusSummary, error) {
s.mu.Lock()
s.GetSummaryHostSoftwareInstallsFuncInvoked = true
@@ -8707,6 +8724,13 @@ func (s *DataStore) GetIncludedHostIDMapForVPPApp(ctx context.Context, vppAppTea
return s.GetIncludedHostIDMapForVPPAppFunc(ctx, vppAppTeamID)
}
func (s *DataStore) GetIncludedHostUUIDMapForAppStoreApp(ctx context.Context, vppAppTeamID uint) (map[string]string, error) {
s.mu.Lock()
s.GetIncludedHostUUIDMapForAppStoreAppFuncInvoked = true
s.mu.Unlock()
return s.GetIncludedHostUUIDMapForAppStoreAppFunc(ctx, vppAppTeamID)
}
func (s *DataStore) GetExcludedHostIDMapForVPPApp(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error) {
s.mu.Lock()
s.GetExcludedHostIDMapForVPPAppFuncInvoked = true
+1 -10
View File
@@ -55,15 +55,6 @@ func (svc *Service) ListActivities(ctx context.Context, opt fleet.ListActivities
return svc.ds.ListActivities(ctx, opt)
}
type ActivityWebhookPayload struct {
Timestamp time.Time `json:"timestamp"`
ActorFullName *string `json:"actor_full_name"`
ActorID *uint `json:"actor_id"`
ActorEmail *string `json:"actor_email"`
Type string `json:"type"`
Details *json.RawMessage `json:"details"`
}
func (svc *Service) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
return newActivity(ctx, user, activity, svc.ds, svc.logger)
}
@@ -108,7 +99,7 @@ func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityD
err := backoff.Retry(
func() error {
if err := server.PostJSONWithTimeout(
context.Background(), webhookURL, &ActivityWebhookPayload{
context.Background(), webhookURL, &fleet.ActivityWebhookPayload{
Timestamp: timestamp,
ActorFullName: userName,
ActorID: userID,
+2 -2
View File
@@ -164,7 +164,7 @@ func Test_logRoleChangeActivities(t *testing.T) {
func TestActivityWebhooks(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
var webhookBody = ActivityWebhookPayload{}
var webhookBody = fleet.ActivityWebhookPayload{}
webhookChannel := make(chan struct{}, 1)
fail429 := false
startMockServer := func(t *testing.T) string {
@@ -172,7 +172,7 @@ func TestActivityWebhooks(t *testing.T) {
srv := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
webhookBody = ActivityWebhookPayload{}
webhookBody = fleet.ActivityWebhookPayload{}
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return // don't send the channel signal
@@ -0,0 +1,224 @@
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, &notFoundError{}
}
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)
}
+34 -4
View File
@@ -35,6 +35,9 @@ import (
"testing"
"time"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
@@ -67,6 +70,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/fleetdm/fleet/v4/server/service/integrationtest/scep_server"
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
"github.com/fleetdm/fleet/v4/server/service/schedule"
"github.com/fleetdm/fleet/v4/server/test"
@@ -115,6 +119,8 @@ type integrationMDMTestSuite struct {
appleGDMFSrv *httptest.Server
mockedDownloadFleetdmMeta fleetdbase.Metadata
scepConfig *eeservice.SCEPConfigService
androidAPIClient *android_mock.Client
proxyCallbackURL string
}
// appleVPPConfigSrvConf is used to configure the mock server that mocks Apple's VPP endpoints.
@@ -191,6 +197,18 @@ func (s *integrationMDMTestSuite) SetupSuite() {
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
wlog = kitlog.NewNopLogger()
}
activityModule := activities.NewActivityModule(s.ds, wlog)
androidMockClient := &android_mock.Client{}
androidMockClient.SetAuthenticationSecretFunc = func(secret string) error {
return nil
}
androidSvc, err := android_service.NewServiceWithClient(wlog, s.ds, androidMockClient, "test-private-key", s.ds, activityModule)
if err != nil {
panic(err)
}
androidSvc.(*android_service.Service).AllowLocalhostServerURL = true
macosJob := &worker.MacosSetupAssistant{
Datastore: s.ds,
Log: wlog,
@@ -207,9 +225,14 @@ func (s *integrationMDMTestSuite) SetupSuite() {
Log: wlog,
Commander: mdmCommander,
}
softwareWorker := &worker.SoftwareWorker{
Datastore: s.ds,
Log: wlog,
AndroidModule: androidSvc,
}
workr := worker.NewWorker(s.ds, wlog)
workr.TestIgnoreUnknownJobs = true
workr.Register(macosJob, appleMDMJob, vppVerifyJob)
workr.Register(macosJob, appleMDMJob, vppVerifyJob, softwareWorker)
s.worker = workr
@@ -257,6 +280,8 @@ func (s *integrationMDMTestSuite) SetupSuite() {
Lq: s.lq,
SoftwareInstallStore: softwareInstallerStore,
BootstrapPackageStore: bootstrapPackageStore,
androidMockClient: androidMockClient,
androidModule: androidSvc,
StartCronSchedules: []TestNewScheduleFunc{
func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
return func() (fleet.CronSchedule, error) {
@@ -385,6 +410,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
s.mdmStorage = mdmStorage
s.mdmCommander = mdmCommander
s.logger = serverLogger
s.androidAPIClient = androidMockClient
fleetdmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status := s.fleetDMNextCSRStatus.Swap(http.StatusOK)
@@ -518,9 +544,13 @@ func (s *integrationMDMTestSuite) SetupSuite() {
// Handle /assets
if strings.Contains(r.URL.Path, "assets") {
w.Header().Set("Content-Type", "application/json")
assets := s.appleVPPConfigSrvConfig.Assets
if adamID := r.URL.Query().Get("adamId"); adamID != "" {
for _, a := range assets {
adamID := r.URL.Query().Get("adamId")
var assets []vpp.Asset
switch adamID {
case "":
assets = s.appleVPPConfigSrvConfig.Assets
default:
for _, a := range s.appleVPPConfigSrvConfig.Assets {
if a.AdamID == adamID {
assets = []vpp.Asset{a}
}
@@ -10,6 +10,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
"github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
@@ -24,13 +25,14 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
logger := log.NewLogfmtLogger(os.Stdout)
proxy := android_mock.Client{}
proxy.InitCommonMocks()
activityModule := activities.NewActivityModule(ds, logger)
androidSvc, err := android_service.NewServiceWithClient(
logger,
ds,
&proxy,
fleetSvc,
"test-private-key",
ds,
activityModule,
)
require.NoError(t, err)
androidSvc.(*android_service.Service).AllowLocalhostServerURL = true
@@ -0,0 +1,114 @@
package activities
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/cenkalti/backoff"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
kithttp "github.com/go-kit/kit/transport/http"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
)
type activityModule struct {
repo ActivityStore
logger kitlog.Logger
}
type ActivityModule interface {
NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error
}
// ActivityStore is the datastore interface needed to handle Fleet activities.
// It is implemented by fleet.Datastore.
type ActivityStore interface {
AppConfig(ctx context.Context) (*fleet.AppConfig, error)
NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time) error
}
func NewActivityModule(repo ActivityStore, logger kitlog.Logger) ActivityModule {
return &activityModule{
repo: repo,
logger: logger,
}
}
var automationActivityAuthor = "Fleet"
func (a *activityModule) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
appConfig, err := a.repo.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get app config")
}
detailsBytes, err := json.Marshal(activity)
if err != nil {
return ctxerr.Wrap(ctx, err, "marshaling activity details")
}
timestamp := time.Now()
if appConfig.WebhookSettings.ActivitiesWebhook.Enable {
webhookURL := appConfig.WebhookSettings.ActivitiesWebhook.DestinationURL
var userID *uint
var userName *string
var userEmail *string
activityType := activity.ActivityName()
if user != nil {
// To support creating activities with users that were deleted. This can happen
// for automatically installed software which uses the author of the upload as the author of
// the installation.
if user.ID != 0 && !user.Deleted {
userID = &user.ID
}
userName = &user.Name
userEmail = &user.Email
} else if automatableActivity, ok := activity.(fleet.AutomatableActivity); ok && automatableActivity.WasFromAutomation() {
userName = &automationActivityAuthor
}
// TODO: webhook module? probably webhook job too tbh since this isn't very resilient
go func() {
retryStrategy := backoff.NewExponentialBackOff()
retryStrategy.MaxElapsedTime = 30 * time.Minute
err := backoff.Retry(
func() error {
if err := server.PostJSONWithTimeout(
context.Background(), webhookURL, &fleet.ActivityWebhookPayload{
Timestamp: timestamp,
ActorFullName: userName,
ActorID: userID,
ActorEmail: userEmail,
Type: activityType,
Details: (*json.RawMessage)(&detailsBytes),
},
); err != nil {
var statusCoder kithttp.StatusCoder
if errors.As(err, &statusCoder) && statusCoder.StatusCode() == http.StatusTooManyRequests {
level.Debug(a.logger).Log("msg", "fire activity webhook", "err", err)
return err
}
return backoff.Permanent(err)
}
return nil
}, retryStrategy,
)
if err != nil {
level.Error(a.logger).Log(
"msg", fmt.Sprintf("fire activity webhook to %s", server.MaskSecretURLParams(webhookURL)), "err",
server.MaskURLError(err).Error(),
)
}
}()
}
// We update the context to indicate that we processed the webhook.
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
return a.repo.NewActivity(ctx, user, activity, detailsBytes, timestamp)
}
+72
View File
@@ -3,6 +3,7 @@ package service
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -33,6 +34,9 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/logging"
"github.com/fleetdm/fleet/v4/server/mail"
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
@@ -55,6 +59,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
"google.golang.org/api/androidmanagement/v1"
)
func newTestService(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) {
@@ -232,6 +237,12 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
}
softwareInstallStore = store
}
var androidModule android.Service
if len(opts) > 0 {
androidModule = opts[0].androidModule
}
svc, err = eeservice.NewService(
svc,
ds,
@@ -250,11 +261,13 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
keyValueStore,
scepConfigService,
digiCertService,
androidModule,
estCAService,
)
if err != nil {
panic(err)
}
}
return svc, ctx
}
@@ -392,6 +405,8 @@ type TestServerOpts struct {
EnableSCIM bool
ConditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
HostIdentity *HostIdentity
androidMockClient *android_mock.Client
androidModule android.Service
ConditionalAccess *ConditionalAccess
}
@@ -424,6 +439,11 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl
if len(opts) > 0 && opts[0].Logger != nil {
logger = opts[0].Logger
}
if len(opts) > 0 {
opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, android_service.GetRoutes(svc, opts[0].androidModule))
}
var mdmPusher nanomdm_push.Pusher
if len(opts) > 0 && opts[0].MDMPusher != nil {
mdmPusher = opts[0].MDMPusher
@@ -1255,3 +1275,55 @@ func getURISchemas() []string {
"z39.50s",
}
}
func createAndroidDeviceID(name string) string {
return "enterprises/mock-enterprise-id/devices/" + name
}
func enrollmentMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage {
deviceInfo.HardwareInfo = &androidmanagement.HardwareInfo{
EnterpriseSpecificId: enterpriseSpecificID,
Brand: "TestBrand",
Model: "TestModel",
SerialNumber: "test-serial",
Hardware: "test-hardware",
}
deviceInfo.SoftwareInfo = &androidmanagement.SoftwareInfo{
AndroidBuildNumber: "test-build",
AndroidVersion: "1",
}
deviceInfo.MemoryInfo = &androidmanagement.MemoryInfo{
TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition
}
deviceInfo.MemoryEvents = []*androidmanagement.MemoryEvent{
{
EventType: "EXTERNAL_STORAGE_DETECTED",
ByteCount: int64(64 * 1024 * 1024 * 1024), // 64GB external/built-in storage total capacity
CreateTime: "2024-01-15T09:00:00Z",
},
{
EventType: "INTERNAL_STORAGE_MEASURED",
ByteCount: int64(10 * 1024 * 1024 * 1024), // 10GB free in system partition
CreateTime: "2024-01-15T10:00:00Z",
},
{
EventType: "EXTERNAL_STORAGE_MEASURED",
ByteCount: int64(25 * 1024 * 1024 * 1024), // 25GB free in external/built-in storage
CreateTime: "2024-01-15T10:00:00Z",
},
}
data, err := json.Marshal(deviceInfo)
require.NoError(t, err)
encodedData := base64.StdEncoding.EncodeToString(data)
return &android.PubSubMessage{
Attributes: map[string]string{
"notificationType": string(android.PubSubEnrollment),
},
Data: encodedData,
}
}
+8 -8
View File
@@ -50,14 +50,14 @@ func (svc *Service) GetAppStoreApps(ctx context.Context, teamID *uint) ([]*fleet
//////////////////////////////////////////////////////////////////////////////
type addAppStoreAppRequest struct {
TeamID *uint `json:"team_id"`
AppStoreID string `json:"app_store_id"`
Platform fleet.AppleDevicePlatform `json:"platform"`
SelfService bool `json:"self_service"`
AutomaticInstall bool `json:"automatic_install"`
LabelsIncludeAny []string `json:"labels_include_any"`
LabelsExcludeAny []string `json:"labels_exclude_any"`
Categories []string `json:"categories"`
TeamID *uint `json:"team_id"`
AppStoreID string `json:"app_store_id"`
Platform fleet.InstallableDevicePlatform `json:"platform"`
SelfService bool `json:"self_service"`
AutomaticInstall bool `json:"automatic_install"`
LabelsIncludeAny []string `json:"labels_include_any"`
LabelsExcludeAny []string `json:"labels_exclude_any"`
Categories []string `json:"categories"`
}
type addAppStoreAppResponse struct {
+185
View File
@@ -0,0 +1,185 @@
package worker
import (
"context"
"encoding/json"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"google.golang.org/api/androidmanagement/v1"
)
const softwareWorkerJobName = "software_worker"
type SoftwareWorkerTask string
type SoftwareWorker struct {
Datastore fleet.Datastore
AndroidModule android.Service
Log kitlog.Logger
}
func (v *SoftwareWorker) Name() string {
return softwareWorkerJobName
}
const makeAndroidAppsAvailableForHostTask SoftwareWorkerTask = "make_android_apps_available_for_host"
const makeAndroidAppAvailableTask SoftwareWorkerTask = "make_android_app_available"
type softwareWorkerArgs struct {
Task SoftwareWorkerTask `json:"task"`
HostUUID string `json:"host_uuid"`
ApplicationID string `json:"application_id"`
EnterpriseName string `json:"enterprise_name"`
AppTeamID uint `json:"app_team_id"`
HostID uint `json:"host_id"`
PolicyID string `json:"policy_id"`
}
func (v *SoftwareWorker) Run(ctx context.Context, argsJSON json.RawMessage) error {
var args softwareWorkerArgs
if err := json.Unmarshal(argsJSON, &args); err != nil {
return ctxerr.Wrap(ctx, err, "unmarshal args")
}
switch args.Task {
case makeAndroidAppsAvailableForHostTask:
return ctxerr.Wrapf(
ctx,
v.makeAndroidAppsAvailableForHost(ctx, args.HostUUID, args.HostID, args.EnterpriseName, args.PolicyID),
"running %s task",
makeAndroidAppsAvailableForHostTask,
)
case makeAndroidAppAvailableTask:
return ctxerr.Wrapf(
ctx,
v.makeAndroidAppAvailable(ctx, args.ApplicationID, args.AppTeamID, args.EnterpriseName),
"running %s task",
makeAndroidAppAvailableTask,
)
default:
return ctxerr.Errorf(ctx, "unknown task: %v", args.Task)
}
}
func (v *SoftwareWorker) makeAndroidAppAvailable(ctx context.Context, applicationID string, appTeamID uint, enterpriseName string) error {
hosts, err := v.Datastore.GetIncludedHostUUIDMapForAppStoreApp(ctx, appTeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: getting android hosts in scope")
}
// Update Android MDM policy to include the app in self service
err = v.AndroidModule.AddAppToAndroidPolicy(ctx, enterpriseName, []string{applicationID}, hosts)
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: add app to android policy")
}
return nil
}
func QueueMakeAndroidAppAvailableJob(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, applicationID string, appTeamID uint, enterpriseName string) error {
args := &softwareWorkerArgs{
Task: makeAndroidAppAvailableTask,
ApplicationID: applicationID,
AppTeamID: appTeamID,
EnterpriseName: enterpriseName,
}
job, err := QueueJob(ctx, ds, softwareWorkerJobName, args)
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}
level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", makeAndroidAppAvailableTask)
return nil
}
func (v *SoftwareWorker) makeAndroidAppsAvailableForHost(ctx context.Context, hostUUID string, hostID uint, enterpriseName, policyID string) error {
if policyID == "1" {
var policy androidmanagement.Policy
policy.StatusReportingSettings = &androidmanagement.StatusReportingSettings{
DeviceSettingsEnabled: true,
MemoryInfoEnabled: true,
NetworkInfoEnabled: true,
DisplayInfoEnabled: true,
PowerManagementEventsEnabled: true,
HardwareStatusEnabled: true,
SystemPropertiesEnabled: true,
SoftwareInfoEnabled: true,
CommonCriteriaModeEnabled: true,
ApplicationReportsEnabled: true,
ApplicationReportingSettings: nil, // only option is "includeRemovedApps", which I opted not to enable (we can diff apps to see removals)
}
policyName := fmt.Sprintf("%s/policies/%s", enterpriseName, hostUUID)
_, err := v.AndroidModule.PatchPolicy(ctx, hostUUID, policyName, &policy, nil)
if err != nil {
return err
}
device := &androidmanagement.Device{
PolicyName: policyName,
// State must be specified when updating a device, otherwise it fails with
// "Illegal state transition from ACTIVE to DEVICE_STATE_UNSPECIFIED"
//
// > Note that when calling enterprises.devices.patch, ACTIVE and
// > DISABLED are the only allowable values.
// TODO(ap): should we send whatever the previous state was? If it was DISABLED,
// we probably don't want to re-enable it by accident. Those are the only
// 2 valid states when patching a device.
State: "ACTIVE",
}
androidHost, err := v.Datastore.AndroidHostLiteByHostUUID(ctx, hostUUID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "get android host by host UUID %s", hostUUID)
}
deviceName := fmt.Sprintf("%s/devices/%s", enterpriseName, androidHost.DeviceID)
_, err = v.AndroidModule.PatchDevice(ctx, hostUUID, deviceName, device)
if err != nil {
return err
}
}
appIDs, err := v.Datastore.GetAndroidAppsInScopeForHost(ctx, hostID)
if err != nil {
return ctxerr.Wrap(ctx, err, "get android apps in scope for host")
}
if len(appIDs) == 0 {
return nil
}
err = v.AndroidModule.AddAppToAndroidPolicy(ctx, enterpriseName, appIDs, map[string]string{hostUUID: hostUUID})
if err != nil {
return ctxerr.Wrap(ctx, err, "add app store app: add app to android policy")
}
return nil
}
func QueueMakeAndroidAppsAvailableForHostJob(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, hostUUID string, hostID uint, enterpriseName, policyID string) error {
args := &softwareWorkerArgs{
Task: makeAndroidAppsAvailableForHostTask,
HostUUID: hostUUID,
HostID: hostID,
EnterpriseName: enterpriseName,
PolicyID: policyID,
}
job, err := QueueJob(ctx, ds, softwareWorkerJobName, args)
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}
level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", makeAndroidAppsAvailableForHostTask)
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package worker
import (
"testing"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
)
func TestSoftwareWorker(t *testing.T) {
ds := mysql.CreateMySQLDS(t)
// call TruncateTables immediately as some DB migrations may create jobs
mysql.TruncateTables(t, ds)
mysql.SetTestABMAssets(t, ds, "fleet")
}
+10 -9
View File
@@ -38,15 +38,16 @@ var (
flagExportName string
validNames = map[fleet.MDMAssetName]struct{}{
fleet.MDMAssetABMCert: {},
fleet.MDMAssetABMTokenDeprecated: {},
fleet.MDMAssetABMKey: {},
fleet.MDMAssetAPNSCert: {},
fleet.MDMAssetAPNSKey: {},
fleet.MDMAssetCACert: {},
fleet.MDMAssetCAKey: {},
fleet.MDMAssetSCEPChallenge: {},
fleet.MDMAssetVPPTokenDeprecated: {},
fleet.MDMAssetABMCert: {},
fleet.MDMAssetABMTokenDeprecated: {},
fleet.MDMAssetABMKey: {},
fleet.MDMAssetAPNSCert: {},
fleet.MDMAssetAPNSKey: {},
fleet.MDMAssetCACert: {},
fleet.MDMAssetCAKey: {},
fleet.MDMAssetSCEPChallenge: {},
fleet.MDMAssetVPPTokenDeprecated: {},
fleet.MDMAssetAndroidFleetServerSecret: {},
}
)
@@ -0,0 +1,99 @@
module.exports = {
friendlyName: 'Get android enterprise applications',
description: 'Gets an android enterprise application',
inputs: {
androidEnterpriseId: {
type: 'string',
required: true,
},
applicationId: {
type: 'string',
required: true,
},
},
exits: {
success: { description: 'The device of an Android enterprise was successfully retrieved.adfasd' },
missingAuthHeader: { description: 'This request was missing an authorization header.', responseType: 'unauthorized'},
unauthorized: { description: 'Invalid authentication token.', responseType: 'unauthorized'},
notFound: { description: 'App not found', responseType: 'notFound' },
deviceNoLongerManaged: { description: 'The device is no longer managed by the Android enterprise.', responseType: 'notFound' },
},
fn: async function ({ androidEnterpriseId, applicationId}) {
// Extract fleetServerSecret from the Authorization header
let authHeader = this.req.get('authorization');
let fleetServerSecret;
if (authHeader && authHeader.startsWith('Bearer')) {
fleetServerSecret = authHeader.replace('Bearer', '').trim();
} else {
throw 'missingAuthHeader';
}
// Authenticate this request
let thisAndroidEnterprise = await AndroidEnterprise.findOne({
androidEnterpriseId: androidEnterpriseId
});
// Return a 404 response if no records are found.
if (!thisAndroidEnterprise) {
throw 'notFound';
}
// Return an unauthorized response if the provided secret does not match.
if (thisAndroidEnterprise.fleetServerSecret !== fleetServerSecret) {
throw 'unauthorized';
}
// Check the list of Android Enterprises managed by Fleet to see if this Android Enterprise is still managed.
let isEnterpriseManagedByFleet = await sails.helpers.androidProxy.getIsEnterpriseManagedByFleet(androidEnterpriseId);
// Return a 404 response if this Android enterprise is no longer managed by Fleet.
if(!isEnterpriseManagedByFleet) {
throw 'notFound';
}
// Get the device for this Android enterprise.
// Note: We're using sails.helpers.flow.build here to handle any errors that occur using google's node library.
let getApplicationsResponse = await sails.helpers.flow.build(async () => {
let { google } = require('googleapis');
let androidmanagement = google.androidmanagement('v1');
let googleAuth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/androidmanagement'],
credentials: {
client_email: sails.config.custom.androidEnterpriseServiceAccountEmailAddress,// eslint-disable-line camelcase
private_key: sails.config.custom.androidEnterpriseServiceAccountPrivateKey,// eslint-disable-line camelcase
},
});
// Acquire the google auth client, and bind it to all future calls
let authClient = await googleAuth.getClient();
google.options({ auth: authClient });
// [?]: https://googleapis.dev/nodejs/googleapis/latest/androidmanagement/classes/Resource$Enterprises$Applications.html#get
let getApplicationsResult = await androidmanagement.enterprises.devices.get({
name: `enterprises/${androidEnterpriseId}/applications/${applicationId}`,
});
return getApplicationsResult.data;
}).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 get an application for an Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`);
});
// Return the device data back to the Fleet server.
return getApplicationsResponse;
}
};
@@ -79,6 +79,7 @@ module.exports = {
let patchPoliciesResponse = await androidmanagement.enterprises.policies.patch({
name: `enterprises/${androidEnterpriseId}/policies/${policyId}`,
requestBody: this.req.body,
updateMask: this.req.param('updateMask') // Pass the update mask to avoid overwriting applications
});
return patchPoliciesResponse.data;
}).intercept({status: 429}, (err)=>{
@@ -0,0 +1,95 @@
module.exports = {
friendlyName: 'Modify android enterprise policy applications',
description: 'Modifies applications in an Android enterprise policy',
inputs: {
androidEnterpriseId: {
type: 'string',
required: true,
},
policyId: {
type: 'string',
required: true,
},
},
exits: {
success: { description: 'The policy applications of an Android enterprise was successfully updated.' },
missingAuthHeader: { description: 'This request was missing an authorization header.', responseType: 'unauthorized'},
unauthorized: { description: 'Invalid authentication token.', responseType: 'unauthorized'},
notFound: { description: 'No Android enterprise found for this Fleet server.', responseType: 'notFound'},
},
fn: async function ({ androidEnterpriseId, policyId}) {
// Extract fleetServerSecret from the Authorization header
let authHeader = this.req.get('authorization');
let fleetServerSecret;
if (authHeader && authHeader.startsWith('Bearer')) {
fleetServerSecret = authHeader.replace('Bearer', '').trim();
} else {
throw 'missingAuthHeader';
}
// Authenticate this request
let thisAndroidEnterprise = await AndroidEnterprise.findOne({
androidEnterpriseId: androidEnterpriseId
});
// Return a 404 response if no records are found.
if (!thisAndroidEnterprise) {
throw 'notFound';
}
// Return an unauthorized response if the provided secret does not match.
if (thisAndroidEnterprise.fleetServerSecret !== fleetServerSecret) {
throw 'unauthorized';
}
// Check the list of Android Enterprises managed by Fleet to see if this Android Enterprise is still managed.
let isEnterpriseManagedByFleet = await sails.helpers.androidProxy.getIsEnterpriseManagedByFleet(androidEnterpriseId);
// Return a 404 response if this Android enterprise is no longer managed by Fleet.
if(!isEnterpriseManagedByFleet) {
throw 'notFound';
}
// Update the policy applications for this Android enterprise.
// Note: We're using sails.helpers.flow.build here to handle any errors that occurr using google's node library.
let modifyApplicationPolicyResponse = await sails.helpers.flow.build(async () => {
let { google } = require('googleapis');
let androidmanagement = google.androidmanagement('v1');
let googleAuth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/androidmanagement'],
credentials: {
client_email: sails.config.custom.androidEnterpriseServiceAccountEmailAddress,// eslint-disable-line camelcase
private_key: sails.config.custom.androidEnterpriseServiceAccountPrivateKey,// eslint-disable-line camelcase
},
});
// Acquire the google auth client, and bind it to all future calls
let authClient = await googleAuth.getClient();
google.options({ auth: authClient });
let patchPoliciesResponse = await androidmanagement.enterprises.policies.modifyPolicyApplications({
name: `enterprises/${androidEnterpriseId}/policies/${policyId}`,
requestBody: this.req.body,
});
return patchPoliciesResponse.data;
}).intercept((err) => {
return new Error(`When attempting to update applications for a policy of Android enterprise (${androidEnterpriseId}), an error occurred. Error: ${err}`);
});
// Return the modified policy back to the Fleet server.
return modifyApplicationPolicyResponse;
}
};
+11 -9
View File
@@ -1060,15 +1060,17 @@ module.exports.routes = {
// ╔═╗╔╗╔╔╦╗╦═╗╔═╗╦╔╦╗ ╔═╗╦═╗╔═╗═╗ ╦╦ ╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
// ╠═╣║║║ ║║╠╦╝║ ║║ ║║ ╠═╝╠╦╝║ ║╔╩╦╝╚╦╝ ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗
// ╩ ╩╝╚╝═╩╝╩╚═╚═╝╩═╩╝ ╩ ╩╚═╚═╝╩ ╚═ ╩ ╚═╝╝╚╝═╩╝╩ ╚═╝╩╝╚╝ ╩ ╚═╝
'POST /api/android/v1/signupUrls': { action: 'android-proxy/create-android-signup-url', csrf: false },
'POST /api/android/v1/enterprises': { action: 'android-proxy/create-android-enterprise', csrf: false },
'GET /api/android/v1/enterprises': { action: 'android-proxy/get-android-enterprises' },
'POST /api/android/v1/enterprises/:androidEnterpriseId/enrollmentTokens': { action: 'android-proxy/create-android-enrollment-token', csrf: false },
'PATCH /api/android/v1/enterprises/:androidEnterpriseId/policies/:policyId': { action: 'android-proxy/modify-android-policies', csrf: false },
'DELETE /api/android/v1/enterprises/:androidEnterpriseId': { action: 'android-proxy/delete-one-android-enterprise', csrf: false },
'GET /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/get-android-device', csrf: false },
'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/signupUrls': { action: 'android-proxy/create-android-signup-url', csrf: false},
'POST /api/android/v1/enterprises': { action: 'android-proxy/create-android-enterprise', csrf: false},
'GET /api/android/v1/enterprises': { action: 'android-proxy/get-android-enterprises'},
'POST /api/android/v1/enterprises/:androidEnterpriseId/enrollmentTokens': { action: 'android-proxy/create-android-enrollment-token', csrf: false},
'PATCH /api/android/v1/enterprises/:androidEnterpriseId/policies/:policyId': { action: 'android-proxy/modify-android-policies', csrf: false},
'DELETE /api/android/v1/enterprises/:androidEnterpriseId': { action: 'android-proxy/delete-one-android-enterprise', csrf: false},
'GET /api/android/v1/enterprises/:androidEnterpriseId/devices/:deviceId': { action: 'android-proxy/get-android-device', csrf: false},
'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},
'GET /api/android/v1/enterprises/:androidEnterpriseId/applications/:applicationId': { action: 'android-proxy/get-enterprise-applications', csrf: false, skipAssets: false},
'POST /api/android/v1/enterprises/:androidEnterpriseId/policies/:policyId': { action: 'android-proxy/modify-enterprise-app-policy', csrf: false, skipAssets: false},
// ╔═╗╔═╗╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
+1 -1
View File
@@ -9,7 +9,7 @@
"@sailshq/connect-redis": "^6.1.3",
"@sailshq/lodash": "^3.10.7",
"@sailshq/socket.io-redis": "^6.1.2",
"googleapis": "148.0.0",
"googleapis": "^165.0.0",
"jsforce": "1.11.1",
"jsonwebtoken": "9.0.2",
"jsrsasign": "11.1.0",