diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index 1cbede5abf54..37daec6859dc 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -94,6 +94,7 @@ "ndes_scep_proxy": null }, "mdm": { + "android_enabled_and_configured": false, "apple_bm_terms_expired": false, "apple_server_url": "", "apple_bm_enabled_and_configured": false, diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json index 77fcb980aa1b..3acb9fc088c0 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json @@ -67,6 +67,7 @@ "ndes_scep_proxy": null }, "mdm": { + "android_enabled_and_configured": false, "apple_bm_terms_expired": false, "apple_server_url": "", "apple_bm_enabled_and_configured": false, diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml index ad20026e99f2..44093ff318bc 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml @@ -19,6 +19,7 @@ spec: ndes_scep_proxy: null zendesk: null mdm: + android_enabled_and_configured: false apple_bm_terms_expired: false apple_server_url: "" apple_bm_enabled_and_configured: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 042be511914c..f830c2d117af 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -19,6 +19,7 @@ spec: ndes_scep_proxy: null zendesk: null mdm: + android_enabled_and_configured: false apple_bm_terms_expired: false apple_server_url: "" apple_bm_enabled_and_configured: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index f8c421065b52..109b6dfd1427 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -46,6 +46,7 @@ "enable_software_inventory": false }, "mdm": { + "android_enabled_and_configured": false, "apple_business_manager": null, "apple_server_url": "", "volume_purchasing_program": null, diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 5f6e877f163b..78c755abf4c6 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -19,6 +19,7 @@ spec: ndes_scep_proxy: null zendesk: null mdm: + android_enabled_and_configured: false apple_business_manager: null apple_server_url: "" volume_purchasing_program: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 50250bc34eaa..cd8044be8620 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -19,6 +19,7 @@ spec: ndes_scep_proxy: null zendesk: null mdm: + android_enabled_and_configured: false apple_business_manager: apple_server_url: "" volume_purchasing_program: diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index b74e3c2d8f5e..d3dff15f7639 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -19,6 +19,7 @@ spec: ndes_scep_proxy: null zendesk: null mdm: + android_enabled_and_configured: false apple_business_manager: apple_server_url: "" volume_purchasing_program: diff --git a/go.mod b/go.mod index 23bd3b951f25..911954879562 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/github/smimesign v0.2.0 github.com/go-git/go-git/v5 v5.13.0 github.com/go-ini/ini v1.67.0 + github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 github.com/go-kit/kit v0.12.0 github.com/go-kit/log v0.2.1 github.com/go-ole/go-ole v1.2.6 diff --git a/go.sum b/go.sum index eb5c9538bb63..bcc3c180ff57 100644 --- a/go.sum +++ b/go.sum @@ -302,6 +302,8 @@ github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkv github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= +github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.7.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= diff --git a/server/archtest/archtest.go b/server/archtest/archtest.go index 0078bed0b898..f55029b0d0ad 100644 --- a/server/archtest/archtest.go +++ b/server/archtest/archtest.go @@ -18,6 +18,7 @@ type PackageTest struct { pkgs []string includeRegex *regexp.Regexp ignorePkgs map[string]struct{} + ignoreXTests map[string]struct{} withTests bool } @@ -49,6 +50,20 @@ func (pt *PackageTest) IgnorePackages(pkgs ...string) *PackageTest { return pt } +func (pt *PackageTest) IgnoreXTests(pkgs ...string) *PackageTest { + if pt.ignoreXTests == nil { + pt.ignoreXTests = make(map[string]struct{}, len(pkgs)) + } + cleanPkgs := make([]string, 0, len(pkgs)) + for _, p := range pkgs { + cleanPkgs = append(cleanPkgs, strings.TrimSuffix(p, "_test")) + } + for _, p := range pt.expandPackages(cleanPkgs) { + pt.ignoreXTests[p] = struct{}{} + } + return pt +} + func (pt *PackageTest) WithTests() *PackageTest { pt.withTests = true return pt @@ -147,6 +162,9 @@ func (pt *PackageTest) read(pChan chan<- *packageDependency, topDependency *pack } // XTestImports are packages with _test suffix that are in the same directory as the package. + if _, ignore := pt.ignoreXTests[dep.name]; ignore { + continue + } for _, i := range pkg.XTestImports { queue.PushBack(&packageDependency{name: i, parent: dep.asXTest()}) } diff --git a/server/authz/policy.rego b/server/authz/policy.rego index be6578f2f5a1..49821732255f 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -1019,3 +1019,13 @@ allow { subject.global_role == [admin, maintainer, gitops][_] action == write } + +## +# Android +## +# Global admins can connect enteprise. +allow { + object.type == "android_enterprise" + subject.global_role == admin + action == write +} diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 5de25d4d0006..acad995c5234 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/fleetdm/fleet/v4/server/contexts/ctxdb" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/jmoiron/sqlx" @@ -108,6 +109,16 @@ func (ds *Datastore) insertOrReplaceConfigAsset(ctx context.Context, tx sqlx.Ext return nil } +func (ds *Datastore) SetAndroidEnabledAndConfigured(ctx context.Context, configured bool) error { + ctx = ctxdb.RequirePrimary(ctx, true) + appConfig, err := ds.AppConfig(ctx) + if err != nil { + return err + } + appConfig.MDM.AndroidEnabledAndConfigured = configured + return ds.SaveAppConfig(ctx, appConfig) +} + func (ds *Datastore) VerifyEnrollSecret(ctx context.Context, secret string) (*fleet.EnrollSecret, error) { var s fleet.EnrollSecret err := sqlx.GetContext(ctx, ds.reader(ctx), &s, "SELECT team_id FROM enroll_secrets WHERE secret = ?", secret) diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 5013fa61399b..b781c4c7d9f1 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -75,7 +75,7 @@ CREATE TABLE `app_config_json` ( PRIMARY KEY (`id`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `calendar_events` ( diff --git a/server/fleet/app.go b/server/fleet/app.go index 65d0d882d524..83158164b849 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -207,6 +207,9 @@ type MDM struct { VolumePurchasingProgram optjson.Slice[MDMAppleVolumePurchasingProgramInfo] `json:"volume_purchasing_program"` + // AndroidEnabledAndConfigured is set to true if Fleet successfully bound to an Android Management Enterprise + AndroidEnabledAndConfigured bool `json:"android_enabled_and_configured"` + ///////////////////////////////////////////////////////////////// // WARNING: If you add to this struct make sure it's taken into // account in the AppConfig Clone implementation! diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 6477923e313b..890966137376 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1962,6 +1962,11 @@ type Datastore interface { // ExpandEmbeddedSecretsAndUpdatedAt is like ExpandEmbeddedSecrets but also // returns the latest updated_at time of the secrets used in the expansion. ExpandEmbeddedSecretsAndUpdatedAt(ctx context.Context, document string) (string, *time.Time, error) + + // ///////////////////////////////////////////////////////////////////////////// + // Android + + SetAndroidEnabledAndConfigured(ctx context.Context, configured bool) error } // MDMAppleStore wraps nanomdm's storage and adds methods to deal with diff --git a/server/mdm/android/android.go b/server/mdm/android/android.go index f55b93d3c248..f0c536e242d1 100644 --- a/server/mdm/android/android.go +++ b/server/mdm/android/android.go @@ -1,8 +1,8 @@ package android type SignupDetails struct { - Url string `json:"url,omitempty"` - Name string `json:"name,omitempty"` + Url string + Name string } type Enterprise struct { @@ -19,6 +19,10 @@ func (e Enterprise) IsValid() bool { return e.EnterpriseID != "" } +func (e Enterprise) AuthzType() string { + return "android_enterprise" +} + type EnrollmentToken struct { Value string `json:"value"` } diff --git a/server/mdm/android/arch_test.go b/server/mdm/android/arch_test.go index bd64c83c2647..f043a26ebef4 100644 --- a/server/mdm/android/arch_test.go +++ b/server/mdm/android/arch_test.go @@ -13,6 +13,8 @@ func TestAllAndroidPackageDependencies(t *testing.T) { t.Parallel() archtest.NewPackageTest(t, "github.com/fleetdm/fleet/v4/server/mdm/android..."). OnlyInclude(regexp.MustCompile(`^github\.com/fleetdm/`)). + WithTests(). + IgnoreXTests("github.com/fleetdm/fleet/v4/server/fleet"). // ignore fleet_test package IgnorePackages( "github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql", "github.com/fleetdm/fleet/v4/server/service/externalsvc", // TODO(#26218): remove this dependency on Jira and Zendesk diff --git a/server/mdm/android/datastore.go b/server/mdm/android/datastore.go index 4a30cfc40479..9b4d9336fb80 100644 --- a/server/mdm/android/datastore.go +++ b/server/mdm/android/datastore.go @@ -7,6 +7,8 @@ import ( type Datastore interface { CreateEnterprise(ctx context.Context) (uint, error) GetEnterpriseByID(ctx context.Context, ID uint) (*Enterprise, error) + GetEnterprise(ctx context.Context) (*Enterprise, error) UpdateEnterprise(ctx context.Context, enterprise *Enterprise) error - ListEnterprises(ctx context.Context) ([]*Enterprise, error) + DeleteEnterprises(ctx context.Context) error + DeleteOtherEnterprises(ctx context.Context, ID uint) error } diff --git a/server/mdm/android/mysql/enterprises.go b/server/mdm/android/mysql/enterprises.go index 2525b4758a0d..bce7fcbe1ea2 100644 --- a/server/mdm/android/mysql/enterprises.go +++ b/server/mdm/android/mysql/enterprises.go @@ -28,7 +28,20 @@ func (ds *Datastore) GetEnterpriseByID(ctx context.Context, id uint) (*android.E case errors.Is(err, sql.ErrNoRows): return nil, notFound("Android enterprise").WithID(id) case err != nil: - return nil, ctxerr.Wrap(ctx, err, "selecting enterprise") + return nil, ctxerr.Wrap(ctx, err, "getting enterprise by id") + } + return &enterprise, nil +} + +func (ds *Datastore) GetEnterprise(ctx context.Context) (*android.Enterprise, error) { + stmt := `SELECT id, enterprise_id FROM android_enterprises WHERE enterprise_id != '' LIMIT 1` + var enterprise android.Enterprise + err := sqlx.GetContext(ctx, ds.reader(ctx), &enterprise, stmt) + switch { + case errors.Is(err, sql.ErrNoRows): + return nil, notFound("Android enterprise") + case err != nil: + return nil, ctxerr.Wrap(ctx, err, "getting active enterprise") } return &enterprise, nil } @@ -51,12 +64,20 @@ func (ds *Datastore) UpdateEnterprise(ctx context.Context, enterprise *android.E return nil } -func (ds *Datastore) ListEnterprises(ctx context.Context) ([]*android.Enterprise, error) { - stmt := `SELECT id, signup_name, enterprise_id FROM android_enterprises` - var enterprises []*android.Enterprise - err := sqlx.SelectContext(ctx, ds.reader(ctx), &enterprises, stmt) +func (ds *Datastore) DeleteOtherEnterprises(ctx context.Context, id uint) error { + stmt := `DELETE FROM android_enterprises WHERE id != ?` + _, err := ds.Writer(ctx).ExecContext(ctx, stmt, id) if err != nil { - return nil, ctxerr.Wrap(ctx, err, "selecting enterprises") + return ctxerr.Wrap(ctx, err, "deleting other enterprises") } - return enterprises, nil + return nil +} + +func (ds *Datastore) DeleteEnterprises(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 } diff --git a/server/mdm/android/mysql/enterprises_test.go b/server/mdm/android/mysql/enterprises_test.go index 4184a50d07ba..2b739313512d 100644 --- a/server/mdm/android/mysql/enterprises_test.go +++ b/server/mdm/android/mysql/enterprises_test.go @@ -21,6 +21,7 @@ func TestEnterprise(t *testing.T) { }{ {"CreateGetEnterprise", testCreateGetEnterprise}, {"UpdateEnterprise", testUpdateEnterprise}, + {"DeleteEnterprises", testDeleteEnterprises}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -65,10 +66,65 @@ func testUpdateEnterprise(t *testing.T, ds *mysql.Datastore) { require.NoError(t, err) assert.Equal(t, enterprise, result) - enterprises, err := ds.ListEnterprises(testCtx()) + result, err = ds.GetEnterprise(testCtx()) require.NoError(t, err) - assert.Len(t, enterprises, 1) - assert.Equal(t, enterprise, enterprises[0]) + assert.Equal(t, enterprise.ID, result.ID) + assert.Equal(t, enterprise.EnterpriseID, result.EnterpriseID) +} + +func testDeleteEnterprises(t *testing.T, ds *mysql.Datastore) { + err := ds.DeleteEnterprises(testCtx()) + require.NoError(t, err) + err = ds.DeleteOtherEnterprises(testCtx(), 9999) + require.NoError(t, err) + + enterprise := createEnterprise(t, ds) + result, err := ds.GetEnterpriseByID(testCtx(), enterprise.ID) + require.NoError(t, err) + assert.Equal(t, enterprise, result) + + // Create enteprise without enterprise_id + id, err := ds.CreateEnterprise(testCtx()) + require.NoError(t, err) + assert.NotZero(t, id) + + tempEnterprise := &android.Enterprise{ + ID: id, // start with an invalid ID + SignupName: "signupUrls/C97372c91c6a85139", + EnterpriseID: "", + } + err = ds.UpdateEnterprise(testCtx(), tempEnterprise) + require.NoError(t, err) + + err = ds.DeleteOtherEnterprises(testCtx(), enterprise.ID) + require.NoError(t, err) + result, err = ds.GetEnterpriseByID(testCtx(), enterprise.ID) + require.NoError(t, err) + assert.Equal(t, enterprise, result) + _, err = ds.GetEnterpriseByID(testCtx(), tempEnterprise.ID) + assert.True(t, fleet.IsNotFound(err)) + + err = ds.DeleteEnterprises(testCtx()) + require.NoError(t, err) + _, err = ds.GetEnterpriseByID(testCtx(), enterprise.ID) + assert.True(t, fleet.IsNotFound(err)) + +} + +func createEnterprise(t *testing.T, ds *mysql.Datastore) *android.Enterprise { + enterprise := &android.Enterprise{ + ID: 9999, // start with an invalid ID + SignupName: "signupUrls/C97372c91c6a85139", + EnterpriseID: "LC04bp524j", + } + id, err := ds.CreateEnterprise(testCtx()) + require.NoError(t, err) + assert.NotZero(t, id) + + enterprise.ID = id + err = ds.UpdateEnterprise(testCtx(), enterprise) + require.NoError(t, err) + return enterprise } func testCtx() context.Context { diff --git a/server/mdm/android/service.go b/server/mdm/android/service.go index 7b8076368c0c..34b980b8f3cd 100644 --- a/server/mdm/android/service.go +++ b/server/mdm/android/service.go @@ -5,6 +5,7 @@ import "context" type Service interface { EnterpriseSignup(ctx context.Context) (*SignupDetails, error) EnterpriseSignupCallback(ctx context.Context, enterpriseID uint, enterpriseToken string) error + DeleteEnterprise(ctx context.Context) error CreateEnrollmentToken(ctx context.Context) (*EnrollmentToken, error) } diff --git a/server/mdm/android/service/endpoint_utils.go b/server/mdm/android/service/endpoint_utils.go index ef69bd4dc5c9..0e0cd7cd333f 100644 --- a/server/mdm/android/service/endpoint_utils.go +++ b/server/mdm/android/service/endpoint_utils.go @@ -6,7 +6,6 @@ import ( "bufio" "compress/gzip" "context" - "encoding/json" "fmt" "io" "net/http" @@ -17,6 +16,8 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/android" "github.com/fleetdm/fleet/v4/server/service/middleware/auth" "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" "github.com/go-kit/kit/endpoint" kithttp "github.com/go-kit/kit/transport/http" "github.com/gorilla/mux" @@ -37,9 +38,7 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa } } - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - return enc.Encode(response) + return json.MarshalWrite(w, response, jsontext.WithIndent(" ")) } // statuser allows response types to implement a custom @@ -98,7 +97,8 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { } req := v.Interface() - if err := json.NewDecoder(body).Decode(req); err != nil { + err = json.UnmarshalRead(body, req) + if err != nil { return nil, endpoint_utils.BadRequestErr("json decoder error", err) } v = reflect.ValueOf(req) diff --git a/server/mdm/android/service/handler.go b/server/mdm/android/service/handler.go index a5b7ef06cb49..66d2b5f9692c 100644 --- a/server/mdm/android/service/handler.go +++ b/server/mdm/android/service/handler.go @@ -24,6 +24,8 @@ func attachFleetAPIRoutes(r *mux.Router, fleetSvc fleet.Service, svc android.Ser ue := newUserAuthenticatedEndpointer(fleetSvc, svc, opts, r, apiVersions()...) ue.GET("/api/_version_/fleet/android_enterprise/signup_url", androidEnterpriseSignupEndpoint, nil) + ue.DELETE("/api/_version_/fleet/android_enterprise", androidDeleteEnterpriseEndpoint, nil) + ue.GET("/api/_version_/fleet/android_enterprise/{id:[0-9]+}/enrollment_token", androidEnrollmentTokenEndpoint, androidEnrollmentTokenRequest{}) diff --git a/server/mdm/android/service/proxy/proxy.go b/server/mdm/android/service/proxy/proxy.go new file mode 100644 index 000000000000..c0eb038bc9bb --- /dev/null +++ b/server/mdm/android/service/proxy/proxy.go @@ -0,0 +1,110 @@ +package proxy + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/go-json-experiment/json" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" + "google.golang.org/api/androidmanagement/v1" + "google.golang.org/api/googleapi" + "google.golang.org/api/option" +) + +// Proxy is a temporary placeholder as an interface to the Google API. +// Once the real proxy is implemented on fleetdm.com, this package will be removed. + +var ( + // Required env vars to use the proxy + androidServiceCredentials = os.Getenv("FLEET_DEV_ANDROID_SERVICE_CREDENTIALS") + androidPubSubTopic = os.Getenv("FLEET_DEV_ANDROID_PUBSUB_TOPIC") + androidProjectID string +) + +type Proxy struct { + logger kitlog.Logger + mgmt *androidmanagement.Service +} + +func NewProxy(ctx context.Context, logger kitlog.Logger) *Proxy { + if androidServiceCredentials == "" || androidPubSubTopic == "" { + return nil + } + + type credentials struct { + ProjectID string `json:"project_id"` + } + + var creds credentials + err := json.Unmarshal([]byte(androidServiceCredentials), &creds) + if err != nil { + level.Error(logger).Log("msg", "unmarshaling android service credentials", "err", err) + return nil + } + androidProjectID = creds.ProjectID + + mgmt, err := androidmanagement.NewService(ctx, option.WithCredentialsJSON([]byte(androidServiceCredentials))) + if err != nil { + level.Error(logger).Log("msg", "creating android management service", "err", err) + return nil + } + return &Proxy{ + logger: logger, + mgmt: mgmt, + } +} + +func (p *Proxy) SignupURLsCreate(callbackURL string) (*android.SignupDetails, error) { + if p == nil || p.mgmt == nil { + return nil, errors.New("android management service not initialized") + } + signupURL, err := p.mgmt.SignupUrls.Create().ProjectId(androidProjectID).CallbackUrl(callbackURL).Do() + if err != nil { + return nil, fmt.Errorf("creating signup url: %w", err) + } + return &android.SignupDetails{ + Url: signupURL.Url, + Name: signupURL.Name, + }, nil +} + +func (p *Proxy) EnterprisesCreate(enabledNotificationTypes []string, enterpriseToken string, signupUrlName string) (string, error) { + if p == nil || p.mgmt == nil { + return "", errors.New("android management service not initialized") + } + enterprise, err := p.mgmt.Enterprises.Create(&androidmanagement.Enterprise{ + EnabledNotificationTypes: enabledNotificationTypes, + PubsubTopic: androidPubSubTopic, + }). + ProjectId(androidProjectID). + EnterpriseToken(enterpriseToken). + SignupUrlName(signupUrlName). + Do() + switch { + case googleapi.IsNotModified(err): + return "", fmt.Errorf("android enterprise %s was already created", signupUrlName) + case err != nil: + return "", fmt.Errorf("creating enterprise: %w", err) + } + return enterprise.Name, nil +} + +func (p *Proxy) EnterpriseDelete(enterpriseID string) error { + if p == nil || p.mgmt == nil { + return errors.New("android management service not initialized") + } + + _, err := p.mgmt.Enterprises.Delete("enterprises/" + enterpriseID).Do() + switch { + case googleapi.IsNotModified(err): + level.Info(p.logger).Log("msg", "enterprise was already deleted", "enterprise_id", enterpriseID) + return nil + case err != nil: + return fmt.Errorf("deleting enterprise %s: %w", enterpriseID, err) + } + return nil +} diff --git a/server/mdm/android/service/service.go b/server/mdm/android/service/service.go index ecfe52c614cb..fbdb4dc1a18f 100644 --- a/server/mdm/android/service/service.go +++ b/server/mdm/android/service/service.go @@ -4,21 +4,24 @@ import ( "context" "errors" "fmt" + "net/http" + "strings" "github.com/fleetdm/fleet/v4/server/authz" + "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/fleetdm/fleet/v4/server/mdm/android/service/proxy" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" - "google.golang.org/api/androidmanagement/v1" ) type Service struct { logger kitlog.Logger authz *authz.Authorizer - mgmt *androidmanagement.Service ds android.Datastore fleetDS fleet.Datastore + proxy *proxy.Proxy } func NewService( @@ -32,16 +35,14 @@ func NewService( return nil, fmt.Errorf("new authorizer: %w", err) } - // mgmt, err := androidmanagement.NewService(ctx) - // if err != nil { - // return nil, ctxerr.Wrap(ctx, err, "creating android management service") - // } - return Service{ + prx := proxy.NewProxy(ctx, logger) + + return &Service{ logger: logger, authz: authorizer, - mgmt: nil, ds: ds, fleetDS: fleetDS, + proxy: prx, }, nil } @@ -51,26 +52,57 @@ type androidResponse struct { func (r androidResponse) Error() error { return r.Err } +func newErrResponse(err error) androidResponse { + return androidResponse{Err: err} +} + type androidEnterpriseSignupResponse struct { - *android.SignupDetails + Url string `json:"android_enterprise_signup_url"` androidResponse } func androidEnterpriseSignupEndpoint(ctx context.Context, _ interface{}, svc android.Service) fleet.Errorer { result, err := svc.EnterpriseSignup(ctx) if err != nil { - return androidResponse{Err: err} + return newErrResponse(err) } - return androidEnterpriseSignupResponse{SignupDetails: result} + return androidEnterpriseSignupResponse{Url: result.Url} } -func (s Service) EnterpriseSignup(ctx context.Context) (*android.SignupDetails, error) { - s.authz.SkipAuthorization(ctx) +func (svc *Service) EnterpriseSignup(ctx context.Context) (*android.SignupDetails, error) { + if err := svc.authz.Authorize(ctx, &android.Enterprise{}, fleet.ActionWrite); err != nil { + return nil, err + } - // TODO: remove me - level.Warn(s.logger).Log("msg", "EnterpriseSignup called") - return nil, errors.New("not implemented") + appConfig, err := svc.fleetDS.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting app config") + } + if appConfig.MDM.AndroidEnabledAndConfigured { + return nil, fleet.NewInvalidArgumentError("android", + "Android is already enabled and configured").WithStatus(http.StatusConflict) + } + + id, err := svc.ds.CreateEnterprise(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "creating enterprise") + } + callbackURL := fmt.Sprintf("%s/api/v1/fleet/android_enterprise/%d/connect", appConfig.ServerSettings.ServerURL, id) + signupDetails, err := svc.proxy.SignupURLsCreate(callbackURL) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "creating signup url") + } + + err = svc.ds.UpdateEnterprise(ctx, &android.Enterprise{ + ID: id, + SignupName: signupDetails.Name, + }) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "updating enterprise") + } + + return signupDetails, nil } type androidEnterpriseSignupCallbackRequest struct { @@ -84,12 +116,93 @@ func androidEnterpriseSignupCallbackEndpoint(ctx context.Context, request interf return androidResponse{Err: err} } -func (s Service) EnterpriseSignupCallback(ctx context.Context, id uint, enterpriseToken string) error { - s.authz.SkipAuthorization(ctx) +func (svc *Service) EnterpriseSignupCallback(ctx context.Context, id uint, enterpriseToken string) error { + // Skip authorization because the callback is called by Google. + // TODO: Add some authorization here so random people can't bind random Android enterprises just for fun. + svc.authz.SkipAuthorization(ctx) - // TODO: remove me - level.Warn(s.logger).Log("msg", "EnterpriseSignupCallback called", "id", id, "enterpriseToken", enterpriseToken) - return errors.New("not implemented") + appConfig, err := svc.fleetDS.AppConfig(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting app config") + } + if appConfig.MDM.AndroidEnabledAndConfigured { + return fleet.NewInvalidArgumentError("android", + "Android is already enabled and configured").WithStatus(http.StatusConflict) + } + + enterprise, err := svc.ds.GetEnterpriseByID(ctx, id) + switch { + case fleet.IsNotFound(err): + return fleet.NewInvalidArgumentError("id", + fmt.Sprintf("Enterprise with ID %d not found", id)).WithStatus(http.StatusNotFound) + case err != nil: + return ctxerr.Wrap(ctx, err, "getting enterprise") + } + + name, err := svc.proxy.EnterprisesCreate( + []string{"ENROLLMENT", "STATUS_REPORT", "COMMAND", "USAGE_LOGS"}, + enterpriseToken, + enterprise.SignupName, + ) + if err != nil { + return ctxerr.Wrap(ctx, err, "creating enterprise") + } + + enterpriseID := strings.TrimPrefix(name, "enterprises/") + enterprise.EnterpriseID = enterpriseID + err = svc.ds.UpdateEnterprise(ctx, enterprise) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating enterprise") + } + + err = svc.ds.DeleteOtherEnterprises(ctx, id) + if err != nil { + return ctxerr.Wrap(ctx, err, "deleting temp enterprises") + } + + err = svc.fleetDS.SetAndroidEnabledAndConfigured(ctx, true) + if err != nil { + return ctxerr.Wrap(ctx, err, "setting android enabled and configured") + } + + return nil +} + +func androidDeleteEnterpriseEndpoint(ctx context.Context, _ interface{}, svc android.Service) fleet.Errorer { + err := svc.DeleteEnterprise(ctx) + return androidResponse{Err: err} +} + +func (svc *Service) DeleteEnterprise(ctx context.Context) error { + if err := svc.authz.Authorize(ctx, &android.Enterprise{}, fleet.ActionWrite); err != nil { + return err + } + + // Get enterprise + enterprise, err := svc.ds.GetEnterprise(ctx) + switch { + case fleet.IsNotFound(err): + // No enterprise to delete + case err != nil: + return ctxerr.Wrap(ctx, err, "getting enterprise") + default: + err = svc.proxy.EnterpriseDelete(enterprise.EnterpriseID) + if err != nil { + return ctxerr.Wrap(ctx, err, "deleting enterprise via Google API") + } + } + + err = svc.ds.DeleteEnterprises(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "deleting enterprises") + } + + err = svc.fleetDS.SetAndroidEnabledAndConfigured(ctx, false) + if err != nil { + return ctxerr.Wrap(ctx, err, "clearing android enabled and configured") + } + + return nil } type androidEnrollmentTokenRequest struct { @@ -109,10 +222,10 @@ func androidEnrollmentTokenEndpoint(ctx context.Context, request interface{}, sv return androidEnrollmentTokenResponse{EnrollmentToken: token} } -func (s Service) CreateEnrollmentToken(ctx context.Context) (*android.EnrollmentToken, error) { - s.authz.SkipAuthorization(ctx) +func (svc *Service) CreateEnrollmentToken(ctx context.Context) (*android.EnrollmentToken, error) { + svc.authz.SkipAuthorization(ctx) // TODO: remove me - level.Warn(s.logger).Log("msg", "CreateEnrollmentToken called") + level.Warn(svc.logger).Log("msg", "CreateEnrollmentToken called") return nil, errors.New("not implemented") } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 6d81dc3cdde5..baf2942386f6 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1223,6 +1223,8 @@ type ExpandEmbeddedSecretsFunc func(ctx context.Context, document string) (strin type ExpandEmbeddedSecretsAndUpdatedAtFunc func(ctx context.Context, document string) (string, *time.Time, error) +type SetAndroidEnabledAndConfiguredFunc func(ctx context.Context, configured bool) error + type DataStore struct { HealthCheckFunc HealthCheckFunc HealthCheckFuncInvoked bool @@ -3027,6 +3029,9 @@ type DataStore struct { ExpandEmbeddedSecretsAndUpdatedAtFunc ExpandEmbeddedSecretsAndUpdatedAtFunc ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked bool + SetAndroidEnabledAndConfiguredFunc SetAndroidEnabledAndConfiguredFunc + SetAndroidEnabledAndConfiguredFuncInvoked bool + mu sync.Mutex } @@ -7236,3 +7241,10 @@ func (s *DataStore) ExpandEmbeddedSecretsAndUpdatedAt(ctx context.Context, docum s.mu.Unlock() return s.ExpandEmbeddedSecretsAndUpdatedAtFunc(ctx, document) } + +func (s *DataStore) SetAndroidEnabledAndConfigured(ctx context.Context, configured bool) error { + s.mu.Lock() + s.SetAndroidEnabledAndConfiguredFuncInvoked = true + s.mu.Unlock() + return s.SetAndroidEnabledAndConfiguredFunc(ctx, configured) +} diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 267f4fb14cdf..accdd0b6e47f 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -13,6 +13,7 @@ import ( "net" "net/http" "net/url" + "os" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/pkg/optjson" @@ -58,6 +59,7 @@ type appConfigResponseFields struct { // SandboxEnabled is true if fleet serve was ran with server.sandbox_enabled=true SandboxEnabled bool `json:"sandbox_enabled,omitempty"` Err error `json:"error,omitempty"` + AndroidEnabled bool `json:"android_enabled,omitempty"` } // UnmarshalJSON implements the json.Unmarshaler interface to make sure we serialize @@ -199,6 +201,7 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se Logging: loggingConfig, Email: emailConfig, SandboxEnabled: svc.SandboxEnabled(), + AndroidEnabled: os.Getenv("FLEET_DEV_ANDROID_ENABLED") == "1", // Temporary feature flag that will be removed. }, } return response, nil @@ -542,6 +545,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle appConfig.MDM.AppleBMTermsExpired = oldAppConfig.MDM.AppleBMTermsExpired appConfig.MDM.AppleBMEnabledAndConfigured = oldAppConfig.MDM.AppleBMEnabledAndConfigured appConfig.MDM.EnabledAndConfigured = oldAppConfig.MDM.EnabledAndConfigured + // ignore MDM.AndroidEnabledAndConfigured because it is set by the server only + appConfig.MDM.AndroidEnabledAndConfigured = oldAppConfig.MDM.AndroidEnabledAndConfigured // do not send a test email in dry-run mode, so this is a good place to stop // (we also delete the removed integrations after that, which we don't want diff --git a/tools/android/android.go b/tools/android/android.go index b65b79805d8e..8ee958f55cfb 100644 --- a/tools/android/android.go +++ b/tools/android/android.go @@ -2,24 +2,38 @@ package main import ( "context" - "encoding/json" "flag" "log" "os" + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" "google.golang.org/api/androidmanagement/v1" "google.golang.org/api/option" ) // Required env vars: var ( - androidServiceCredentials = os.Getenv("FLEET_ANDROID_SERVICE_CREDENTIALS") - androidProjectID = os.Getenv("FLEET_ANDROID_PROJECT_ID") + androidServiceCredentials = os.Getenv("FLEET_DEV_ANDROID_SERVICE_CREDENTIALS") + androidProjectID string ) func main() { - if androidServiceCredentials == "" || androidProjectID == "" { - log.Fatal("FLEET_ANDROID_SERVICE_CREDENTIALS and FLEET_ANDROID_PROJECT_ID must be set") + if androidServiceCredentials == "" { + log.Fatal("FLEET_DEV_ANDROID_SERVICE_CREDENTIALS must be set") + } + + type credentials struct { + ProjectID string `json:"project_id"` + } + var creds credentials + err := json.Unmarshal([]byte(androidServiceCredentials), &creds) + if err != nil { + log.Fatalf("unmarshaling android service credentials: %s", err) + } + androidProjectID = creds.ProjectID + if androidProjectID == "" { + log.Fatal("project_id not found in android service credentials") } command := flag.String("command", "", "") @@ -106,7 +120,7 @@ func devicesList(mgmt *androidmanagement.Service, enterpriseID string) { return } for _, device := range result.Devices { - data, err := json.MarshalIndent(device, "", " ") + data, err := json.Marshal(device, jsontext.WithIndent(" ")) if err != nil { log.Fatalf("Error marshalling device: %v", err) } @@ -136,7 +150,7 @@ func devicesRelinquishOwnership(mgmt *androidmanagement.Service, enterpriseID, d if err != nil { log.Fatalf("Error issuing command: %v", err) } - data, err := json.MarshalIndent(operation, "", " ") + data, err := json.Marshal(operation, jsontext.WithIndent(" ")) if err != nil { log.Fatalf("Error marshalling operation: %v", err) } diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 6c48aadd230a..09ce197b656a 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -174,6 +174,7 @@ github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server/fleet.MDMAppleVolumePurchasingProgramInfo] Value []fleet.MDMAppleVolumePurchasingProgramInfo github.com/fleetdm/fleet/v4/server/fleet/MDMAppleVolumePurchasingProgramInfo Location string github.com/fleetdm/fleet/v4/server/fleet/MDMAppleVolumePurchasingProgramInfo Teams []string +github.com/fleetdm/fleet/v4/server/fleet/MDM AndroidEnabledAndConfigured bool github.com/fleetdm/fleet/v4/server/fleet/AppConfig Scripts optjson.Slice[string] github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Set bool github.com/fleetdm/fleet/v4/pkg/optjson/Slice[string] Valid bool