Basic Android MDM on/off functionality (#26309)
For #26218 Basic Android MDM on/off backend functionality. Manually tested. The following env vars must be set: ``` FLEET_DEV_ANDROID_ENABLED=1 FLEET_DEV_ANDROID_SERVICE_CREDENTIALS=$(cat credentials.json) FLEET_DEV_ANDROID_PUBSUB_TOPIC=projects/your-project/topics/your-topic ``` I picked https://github.com/go-json-experiment/json as the JSON library, which seems like the safest option. - will become json/v2 at some point - currently used in production by other companies, like Tailscale - well-maintained - Some context here: https://github.com/fleetdm/fleet/issues/25512 Plan for next work: - refactoring from 1st PR - add pubsub with device enroll -> spec proxy for fleetdm.com - come back to this sub-task to add tests and finish TODOs # Checklist for submitter - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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` (
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -1968,6 +1968,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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -1227,6 +1227,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
|
||||
@@ -3037,6 +3039,9 @@ type DataStore struct {
|
||||
ExpandEmbeddedSecretsAndUpdatedAtFunc ExpandEmbeddedSecretsAndUpdatedAtFunc
|
||||
ExpandEmbeddedSecretsAndUpdatedAtFuncInvoked bool
|
||||
|
||||
SetAndroidEnabledAndConfiguredFunc SetAndroidEnabledAndConfiguredFunc
|
||||
SetAndroidEnabledAndConfiguredFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -7260,3 +7265,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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user