fleet/server/mdm/android/service/proxy/proxy.go
Victor Lyuboslavsky fc33df1981
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
2025-02-18 09:43:11 -06:00

110 lines
3.2 KiB
Go

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
}