Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
getvictor committed Jan 29, 2025
1 parent 1458d3d commit 0af94a9
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 13 deletions.
26 changes: 26 additions & 0 deletions cmd/fleet/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
eewebhooks "github.com/fleetdm/fleet/v4/ee/server/webhooks"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/android"
"github.com/fleetdm/fleet/v4/server/android/job"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
Expand Down Expand Up @@ -1478,3 +1480,27 @@ func newMaintainedAppSchedule(

return s, nil
}

func newMDMAndroidManagerSchedule(
ctx context.Context,
instanceID string,
ds fleet.Datastore,
androidDS android.Datastore,
logger kitlog.Logger,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronMDMAndroidManager)
defaultInterval = 30 * time.Second
)

logger = kitlog.With(logger, "cron", name)
s := schedule.New(
ctx, name, instanceID, defaultInterval, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("manage_android", func(ctx context.Context) error {
return job.ReconcileDevices(ctx, ds, androidDS, logger)
}),
)

return s, nil
}
12 changes: 12 additions & 0 deletions cmd/fleet/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,18 @@ the way that the Fleet server works.
initFatal(err, "failed to register mdm_windows_profile_manager schedule")
}

if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
return newMDMAndroidManagerSchedule(
ctx,
instanceID,
ds,
androidDS,
logger,
)
}); err != nil {
initFatal(err, fmt.Sprintf("failed to register %s schedule", fleet.CronMDMAndroidManager))
}

if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
return newMDMAPNsPusher(
ctx,
Expand Down
8 changes: 8 additions & 0 deletions server/android/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ type Enterprise struct {
SignupName string `db:"signup_name"`
EnterpriseID string `db:"enterprise_id"`
}

func (e Enterprise) Name() string {
return "enterprises/" + e.EnterpriseID
}

type EnrollmentToken struct {
Value string `json:"value"`
}
1 change: 1 addition & 0 deletions server/android/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Datastore interface {
CreateEnterprise(ctx context.Context) (uint, error)
GetEnterpriseByID(ctx context.Context, ID uint) (*Enterprise, error)
UpdateEnterprise(ctx context.Context, enterprise *Enterprise) error
ListEnterprises(ctx context.Context) ([]*Enterprise, error)
}

type MigrationStatus struct {
Expand Down
52 changes: 52 additions & 0 deletions server/android/job/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package job

import (
"context"
"errors"
"os"

"github.com/fleetdm/fleet/v4/server/android"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
kitlog "github.com/go-kit/log"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/option"
)

var (
// Required env vars
androidServiceCredentials = os.Getenv("FLEET_ANDROID_SERVICE_CREDENTIALS")
androidProjectID = os.Getenv("FLEET_ANDROID_PROJECT_ID")
)

func ReconcileDevices(ctx context.Context, ds fleet.Datastore, androidDS android.Datastore, logger kitlog.Logger) error {
if androidServiceCredentials == "" || androidProjectID == "" {
return errors.New("FLEET_ANDROID_SERVICE_CREDENTIALS and FLEET_ANDROID_PROJECT_ID must be set")
}

mgmt, err := androidmanagement.NewService(ctx, option.WithCredentialsJSON([]byte(androidServiceCredentials)))
if err != nil {
return ctxerr.Wrap(ctx, err, "creating android management service")
}

enterprises, err := androidDS.ListEnterprises(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "listing enterprises")
}

for _, enterprise := range enterprises {
// Note: we can optimize this by using Fields to retrieve partial data https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
devices, err := mgmt.Enterprises.Devices.List(enterprise.Name()).Do()
if err != nil {
return ctxerr.Wrap(ctx, err, "listing devices with Google API")
}

for _, device := range devices.Devices {
logger.Log("msg", "device", "device", device)
}

// For each device, check whether it is in Fleet. If not, add it
}

return nil
}
10 changes: 10 additions & 0 deletions server/android/mysql/enterprise.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,13 @@ 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)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting enterprises")
}
return enterprises, nil
}
13 changes: 7 additions & 6 deletions server/android/mysql/enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ func testCreateGetEnterprise(t *testing.T, ds *Datastore) {

result, err := ds.GetEnterpriseByID(testCtx(), id)
require.NoError(t, err)
assert.Equal(t, id, result.ID)
assert.Empty(t, result.SignupName)
assert.Empty(t, result.EnterpriseID)
assert.Equal(t, &android.Enterprise{ID: id}, result)
}

func testUpdateEnterprise(t *testing.T, ds *Datastore) {
Expand All @@ -62,7 +60,10 @@ func testUpdateEnterprise(t *testing.T, ds *Datastore) {

result, err := ds.GetEnterpriseByID(testCtx(), enterprise.ID)
require.NoError(t, err)
assert.Equal(t, enterprise.ID, result.ID)
assert.Equal(t, enterprise.SignupName, result.SignupName)
assert.Equal(t, enterprise.EnterpriseID, result.EnterpriseID)
assert.Equal(t, enterprise, result)

enterprises, err := ds.ListEnterprises(testCtx())
require.NoError(t, err)
assert.Len(t, enterprises, 1)
assert.Equal(t, enterprise, enterprises[0])
}
4 changes: 4 additions & 0 deletions server/android/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ import "context"
type Service interface {
EnterpriseSignup(ctx context.Context) (*SignupDetails, error)
EnterpriseSignupCallback(ctx context.Context, enterpriseID uint, enterpriseToken string) error

CreateOrUpdatePolicy(ctx context.Context, enterpriseID uint) error

CreateEnrollmentToken(ctx context.Context, fleetEnterpriseID uint) (*EnrollmentToken, error)
}
81 changes: 76 additions & 5 deletions server/android/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
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"
)

Expand All @@ -25,11 +26,13 @@ type Service struct {
fleetDS fleet.Datastore
}

// Required env vars:
var (
// Required env vars
androidServiceCredentials = os.Getenv("FLEET_ANDROID_SERVICE_CREDENTIALS")
androidProjectID = os.Getenv("FLEET_ANDROID_PROJECT_ID")
androidPubSubTopic = os.Getenv("FLEET_ANDROID_PUBSUB_TOPIC")

// Optional env vars
androidPubSubTopic = os.Getenv("FLEET_ANDROID_PUBSUB_TOPIC")
)

func NewService(
Expand All @@ -40,7 +43,7 @@ func NewService(
fleetDS fleet.Datastore,
) (android.Service, error) {
// TODO: Android management service should only be created when needed.
if androidServiceCredentials == "" || androidProjectID == "" || androidPubSubTopic == "" {
if androidServiceCredentials == "" || androidProjectID == "" {
level.Error(logger).Log("msg",
"FLEET_ANDROID_SERVICE_CREDENTIALS, FLEET_ANDROID_PROJECT_ID, and FLEET_ANDROID_PUBSUB_TOPIC environment variables must be set to use Android management")
return nil, nil
Expand Down Expand Up @@ -111,10 +114,13 @@ func (s Service) EnterpriseSignupCallback(ctx context.Context, id uint, enterpri

gEnterprise := &androidmanagement.Enterprise{
EnabledNotificationTypes: []string{"ENROLLMENT", "STATUS_REPORT", "COMMAND", "USAGE_LOGS"},
PubsubTopic: androidPubSubTopic,
PubsubTopic: androidPubSubTopic, // will be ignored if empty
}
gEnterprise, err = s.mgmt.Enterprises.Create(gEnterprise).ProjectId(androidProjectID).EnterpriseToken(enterpriseToken).SignupUrlName(enterprise.SignupName).Do()
if err != nil {
switch {
case googleapi.IsNotModified(err):
s.logger.Log("msg", "Android enterprise was already created", "enterprise_id", enterprise.EnterpriseID)
case err != nil:
return ctxerr.Wrap(ctx, err, "creating enterprise via Google API")
}

Expand All @@ -129,3 +135,68 @@ func (s Service) EnterpriseSignupCallback(ctx context.Context, id uint, enterpri

return nil
}

func (s Service) CreateOrUpdatePolicy(ctx context.Context, fleetEnterpriseID uint) error {
s.authz.SkipAuthorization(ctx)

enterprise, err := s.ds.GetEnterpriseByID(ctx, fleetEnterpriseID)
switch {
case fleet.IsNotFound(err):
return fleet.NewInvalidArgumentError("id",
fmt.Sprintf("Enterprise with ID %d not found", fleetEnterpriseID)).WithStatus(http.StatusNotFound)
case err != nil:
return ctxerr.Wrap(ctx, err, "getting enterprise")
}

policyName := fmt.Sprintf("enterprises/%s/policies/default", enterprise.EnterpriseID)
_, err = s.mgmt.Enterprises.Policies.Patch(policyName, &androidmanagement.Policy{
CameraAccess: "CAMERA_ACCESS_DISABLED",
StatusReportingSettings: &androidmanagement.StatusReportingSettings{
ApplicationReportsEnabled: true,
DeviceSettingsEnabled: true,
SoftwareInfoEnabled: true,
MemoryInfoEnabled: true,
NetworkInfoEnabled: true,
DisplayInfoEnabled: true,
PowerManagementEventsEnabled: true,
HardwareStatusEnabled: true,
SystemPropertiesEnabled: true,
ApplicationReportingSettings: &androidmanagement.ApplicationReportingSettings{
IncludeRemovedApps: true,
},
CommonCriteriaModeEnabled: true,
},
}).Do()
switch {
case googleapi.IsNotModified(err):
s.logger.Log("msg", "Android policy not modified", "enterprise_id", enterprise.EnterpriseID)
case err != nil:
return ctxerr.Wrap(ctx, err, "creating or updating policy via Google API")
}

return nil
}

func (s Service) CreateEnrollmentToken(ctx context.Context, fleetEnterpriseID uint) (*android.EnrollmentToken, error) {
s.authz.SkipAuthorization(ctx)
enterprise, err := s.ds.GetEnterpriseByID(ctx, fleetEnterpriseID)
switch {
case fleet.IsNotFound(err):
return nil, fleet.NewInvalidArgumentError("id",
fmt.Sprintf("Enterprise with ID %d not found", fleetEnterpriseID)).WithStatus(http.StatusNotFound)
case err != nil:
return nil, ctxerr.Wrap(ctx, err, "getting enterprise")
}

token, err := s.mgmt.Enterprises.EnrollmentTokens.Create(enterprise.Name(), &androidmanagement.EnrollmentToken{
AllowPersonalUsage: "PERSONAL_USAGE_ALLOWED",
PolicyName: enterprise.Name() + "/policies/default",
}).Do()
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating enrollment token via Google API")
}

return &android.EnrollmentToken{
Value: token.Value,
}, nil
}
1 change: 1 addition & 0 deletions server/fleet/cron_schedules.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
CronCalendar CronScheduleName = "calendar"
CronUninstallSoftwareMigration CronScheduleName = "uninstall_software_migration"
CronMaintainedApps CronScheduleName = "maintained_apps"
CronMDMAndroidManager CronScheduleName = "mdm_android_manager"
)

type CronSchedulesService interface {
Expand Down
32 changes: 30 additions & 2 deletions server/service/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type androidResponse struct {
func (r androidResponse) error() error { return r.Err }

type androidEnterpriseSignupResponse struct {
android.SignupDetails
*android.SignupDetails
androidResponse
}

Expand All @@ -27,7 +27,7 @@ func androidEnterpriseSignupEndpoint(ctx context.Context, _ interface{}, svc fle
if err != nil {
return androidResponse{Err: err}, nil
}
return androidEnterpriseSignupResponse{SignupDetails: *result}, nil
return androidEnterpriseSignupResponse{SignupDetails: result}, nil
}

type androidEnterpriseSignupCallbackRequest struct {
Expand All @@ -40,3 +40,31 @@ func androidEnterpriseSignupCallbackEndpoint(ctx context.Context, request interf
err := svc.Android().EnterpriseSignupCallback(ctx, req.ID, req.EnterpriseToken)
return androidResponse{Err: err}, nil
}

type androidPoliciesRequest struct {
EnterpriseID uint `url:"id"`
}

func androidPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*androidPoliciesRequest)
err := svc.Android().CreateOrUpdatePolicy(ctx, req.EnterpriseID)
return androidResponse{Err: err}, nil
}

type androidEnrollmentTokenRequest struct {
EnterpriseID uint `url:"id"`
}

type androidEnrollmentTokenResponse struct {
*android.EnrollmentToken
androidResponse
}

func androidEnrollmentTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*androidEnrollmentTokenRequest)
token, err := svc.Android().CreateEnrollmentToken(ctx, req.EnterpriseID)
if err != nil {
return androidResponse{Err: err}, nil
}
return androidEnrollmentTokenResponse{EnrollmentToken: token}, nil
}
3 changes: 3 additions & 0 deletions server/service/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC

// Android management
ue.GET("/api/_version_/fleet/android/enterprise/signup", androidEnterpriseSignupEndpoint, nil)
ue.PATCH("/api/_version_/fleet/android/enterprise/{id:[0-9]+}/policies/default", androidPoliciesEndpoint, androidPoliciesRequest{})
ue.GET("/api/_version_/fleet/android/enterprise/{id:[0-9]+}/enrollment_token", androidEnrollmentTokenEndpoint,
androidEnrollmentTokenRequest{})

// Only Fleet MDM specific endpoints should be within the root /mdm/ path.
// NOTE: remember to update
Expand Down
Loading

0 comments on commit 0af94a9

Please sign in to comment.