Add command get mdm-apple-bm to fleetctl (#8949)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added the `fleetctl get mdm_apple_bm` command to retrieve the Apple Business Manager configuration information.
|
||||
+5
-8
@@ -51,7 +51,6 @@ import (
|
||||
"github.com/go-kit/kit/log/level"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/kolide/kit/version"
|
||||
nanodep_client "github.com/micromdm/nanodep/client"
|
||||
"github.com/micromdm/nanomdm/cryptoutil"
|
||||
"github.com/micromdm/nanomdm/push/buford"
|
||||
nanomdm_pushsvc "github.com/micromdm/nanomdm/push/service"
|
||||
@@ -394,7 +393,6 @@ the way that the Fleet server works.
|
||||
appleSCEPKeyPEM []byte
|
||||
appleAPNsCertPEM []byte
|
||||
appleAPNsKeyPEM []byte
|
||||
appleBMToken *nanodep_client.OAuth1Tokens
|
||||
depStorage *mysql.NanoDEPStorage
|
||||
mdmStorage *mysql.NanoMDMStorage
|
||||
mdmPushService *nanomdm_pushsvc.PushService
|
||||
@@ -448,7 +446,10 @@ the way that the Fleet server works.
|
||||
if err != nil {
|
||||
initFatal(err, "validate Apple BM token, certificate and key")
|
||||
}
|
||||
appleBMToken = tok
|
||||
depStorage, err = mds.NewMDMAppleDEPStorage(*tok)
|
||||
if err != nil {
|
||||
initFatal(err, "initialize Apple BM DEP storage")
|
||||
}
|
||||
}
|
||||
|
||||
if config.MDMApple.Enable {
|
||||
@@ -471,10 +472,6 @@ the way that the Fleet server works.
|
||||
if err != nil {
|
||||
initFatal(err, "initialize mdm apple MySQL storage")
|
||||
}
|
||||
depStorage, err = mds.NewMDMAppleDEPStorage(*appleBMToken)
|
||||
if err != nil {
|
||||
initFatal(err, "initialize mdm apple dep storage")
|
||||
}
|
||||
nanoMDMLogger := NewNanoMDMLogger(kitlog.With(logger, "component", "apple-mdm-push"))
|
||||
pushProviderFactory := buford.NewPushProviderFactory()
|
||||
mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger)
|
||||
@@ -516,7 +513,7 @@ the way that the Fleet server works.
|
||||
}
|
||||
|
||||
if license.IsPremium() {
|
||||
svc, err = eeservice.NewService(svc, ds, logger, config, mailService, clock.C)
|
||||
svc, err = eeservice.NewService(svc, ds, logger, config, mailService, clock.C, depStorage)
|
||||
if err != nil {
|
||||
initFatal(err, "initial Fleet Premium service")
|
||||
}
|
||||
|
||||
+57
-1
@@ -280,6 +280,7 @@ func getCommand() *cli.Command {
|
||||
getTeamsCommand(),
|
||||
getSoftwareCommand(),
|
||||
getMDMAppleCommand(),
|
||||
getMDMAppleBMCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1072,7 +1073,7 @@ func getMDMAppleCommand() *cli.Command {
|
||||
log(c, "Error: No Apple Push Notification service (APNs) certificate found. Use `fleetctl generate mdm-apple` and then `fleet serve` with `mdm` configuration to turn on MDM features.\n")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return fmt.Errorf("could not get Apple MDM information: %w", err)
|
||||
}
|
||||
|
||||
printKeyValueTable(c, [][]string{
|
||||
@@ -1095,3 +1096,58 @@ func getMDMAppleCommand() *cli.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getMDMAppleBMCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "mdm_apple_bm",
|
||||
Hidden: true, // TODO: temporary, until the MDM feature is officially released
|
||||
Aliases: []string{"mdm-apple-bm"},
|
||||
Usage: "Show information about Apple Business Manager for automatic enrollment",
|
||||
Flags: []cli.Flag{
|
||||
configFlag(),
|
||||
contextFlag(),
|
||||
debugFlag(),
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
const expirationWarning = 30 * 24 * time.Hour // 30 days
|
||||
|
||||
client, err := clientFromCLI(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bm, err := client.GetAppleBM()
|
||||
if err != nil {
|
||||
var nfe service.NotFoundErr
|
||||
if errors.As(err, &nfe) {
|
||||
log(c, "Error: No Apple Business Manager server token found. Use `fleetctl generate mdm-apple-bm` and then `fleet serve` with `mdm` configuration to automatically enroll macOS hosts to Fleet.\n")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("could not get Apple BM information: %w", err)
|
||||
}
|
||||
|
||||
defaultTeam := bm.DefaultTeam
|
||||
if defaultTeam == "" {
|
||||
defaultTeam = "No team"
|
||||
}
|
||||
printKeyValueTable(c, [][]string{
|
||||
{"Apple ID:", bm.AppleID},
|
||||
{"Organization name:", bm.OrgName},
|
||||
{"MDM server URL:", bm.MDMServerURL},
|
||||
{"Renew date:", bm.RenewDate.Format("January 2, 2006")},
|
||||
{"Default team:", defaultTeam},
|
||||
})
|
||||
|
||||
warnDate := time.Now().Add(expirationWarning)
|
||||
if bm.RenewDate.Before(time.Now()) {
|
||||
// certificate is expired, print an error
|
||||
color.New(color.FgRed).Fprintln(c.App.Writer, "\nERROR: Your Apple Business Manager (ABM) server token is expired. Laptops newly purchased via ABM will not automatically enroll in Fleet. To renew your ABM server token, follow these instructions: https://fleetdm.com/docs/using-fleet/faq#how-can-i-renew-my-apple-business-manager-server-token")
|
||||
} else if bm.RenewDate.Before(warnDate) {
|
||||
// certificate will soon expire, print a warning
|
||||
color.New(color.FgYellow).Fprintln(c.App.Writer, "\nWARNING: Your Apple Business Manager (ABM) server token is less than 30 days from expiration. If it expires, laptops newly purchased via ABM will not automatically enroll in Fleet. To renew your ABM server token, follow these instructions: https://fleetdm.com/docs/using-fleet/faq#how-can-i-renew-my-apple-business-manager-server-token")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1461,6 +1461,24 @@ func TestGetAppleMDM(t *testing.T) {
|
||||
assert.Contains(t, runAppForTest(t, []string{"get", "mdm_apple"}), expected)
|
||||
}
|
||||
|
||||
func TestGetAppleBM(t *testing.T) {
|
||||
t.Run("free license", func(t *testing.T) {
|
||||
runServerWithMockedDS(t)
|
||||
|
||||
expected := `could not get Apple BM information: missing or invalid license`
|
||||
_, err := runAppNoChecks([]string{"get", "mdm_apple_bm"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), expected)
|
||||
})
|
||||
|
||||
t.Run("premium license", func(t *testing.T) {
|
||||
runServerWithMockedDS(t, &service.TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
|
||||
expected := `No Apple Business Manager server token found`
|
||||
assert.Contains(t, runAppForTest(t, []string{"get", "mdm_apple_bm"}), expected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCarves(t *testing.T) {
|
||||
_, ds := runServerWithMockedDS(t)
|
||||
|
||||
|
||||
@@ -525,6 +525,7 @@ Delete pack by name.
|
||||
The MDM endpoints exist to support the related command-line interface sub-commands of `fleetctl`, such as `fleetctl generate mdm-apple` and `fleetctl get mdm-apple`, as well as the Web UI.
|
||||
|
||||
- [Get Apple MDM](#get-apple-mdm)
|
||||
- [Get Apple BM](#get-apple-bm)
|
||||
|
||||
### Get Apple MDM
|
||||
|
||||
@@ -551,6 +552,34 @@ None.
|
||||
}
|
||||
```
|
||||
|
||||
### Get Apple BM
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple_bm`
|
||||
|
||||
#### Parameters
|
||||
|
||||
None.
|
||||
|
||||
#### Example
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple_bm`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
{
|
||||
"apple_id": "example@fleetdm.com",
|
||||
"org_name": "Fleet Device Management",
|
||||
"mdm_server_url": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2023-11-29T00:00:00Z",
|
||||
"default_team": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Get or apply configuration files
|
||||
|
||||
These API routes are used by the `fleetctl` CLI tool. Users can manage Fleet with `fleetctl` and [configuration files in YAML syntax](https://fleetdm.com/docs/using-fleet/configuration-files/).
|
||||
|
||||
@@ -42,6 +42,7 @@ Users with the Admin role receive all permissions.
|
||||
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
|
||||
| Retrieve contents from file carving | | | ✅ |
|
||||
| View Apple mobile device management (MDM) certificate information | | | ✅ |
|
||||
| View Apple business manager (BM) information | | | ✅ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package service
|
||||
|
||||
type notFoundError struct{}
|
||||
|
||||
func (e notFoundError) Error() string {
|
||||
return "not found"
|
||||
}
|
||||
|
||||
// IsNotFound implements the service.IsNotFound interface (from the non-premium
|
||||
// service package) so that the handler returns 404 for this error.
|
||||
func (e notFoundError) IsNotFound() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/micromdm/nanodep/client"
|
||||
"github.com/micromdm/nanodep/storage"
|
||||
)
|
||||
|
||||
func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if there is no apple bm config, fail with a 404
|
||||
if !svc.config.MDM.IsAppleBMSet() {
|
||||
return nil, notFoundError{}
|
||||
}
|
||||
|
||||
appCfg, err := svc.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tok, err := svc.config.MDM.AppleBM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appleBM, err := getAppleBMAccountDetail(ctx, svc.depStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fill the rest of the AppleBM fields
|
||||
appleBM.RenewDate = tok.AccessTokenExpiry
|
||||
// TODO: default team will have to be set when https://github.com/fleetdm/fleet/issues/8733
|
||||
// is implemented.
|
||||
appleBM.DefaultTeam = ""
|
||||
appleBM.MDMServerURL = appCfg.ServerSettings.ServerURL + apple_mdm.MDMPath
|
||||
|
||||
return appleBM, nil
|
||||
}
|
||||
|
||||
func getAppleBMAccountDetail(ctx context.Context, depStorage storage.AllStorage) (*fleet.AppleBM, error) {
|
||||
httpClient := fleethttp.NewClient()
|
||||
depTransport := client.NewTransport(httpClient.Transport, httpClient, depStorage, nil)
|
||||
depClient := client.NewClient(fleethttp.NewClient(), depTransport)
|
||||
|
||||
req, err := client.NewRequestWithContext(ctx, apple_mdm.DEPName, depStorage, "GET", "/account", nil)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create apple GET /account request")
|
||||
}
|
||||
res, err := depClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "execute apple GET /account request")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// TODO: if it fails in a way that indicates the token is invalid/expired
|
||||
// (403 Forbidden), eventually we should surface that to the user.
|
||||
if res.StatusCode >= 400 {
|
||||
// read up to 512 bytes of the response body to get better error message if possible
|
||||
body, _ := ioutil.ReadAll(io.LimitReader(res.Body, 512))
|
||||
return nil, ctxerr.Wrapf(ctx, err, "apple GET /account request failed: status: %d; body: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var account struct {
|
||||
AdminID string `json:"admin_id"`
|
||||
FacilitatorID string `json:"facilitator_id"`
|
||||
OrgName string `json:"org_name"`
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&account); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "decode apple GET /account response")
|
||||
}
|
||||
|
||||
if account.AdminID == "" {
|
||||
// fallback to facilitator ID, as this is the same information but for
|
||||
// older versions of the Apple API.
|
||||
// https://github.com/fleetdm/fleet/issues/7515#issuecomment-1346579398
|
||||
account.AdminID = account.FacilitatorID
|
||||
}
|
||||
return &fleet.AppleBM{
|
||||
AppleID: account.AdminID,
|
||||
OrgName: account.OrgName,
|
||||
}, nil
|
||||
}
|
||||
@@ -8,16 +8,18 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/micromdm/nanodep/storage"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
fleet.Service
|
||||
|
||||
ds fleet.Datastore
|
||||
logger kitlog.Logger
|
||||
config config.FleetConfig
|
||||
clock clock.Clock
|
||||
authz *authz.Authorizer
|
||||
ds fleet.Datastore
|
||||
logger kitlog.Logger
|
||||
config config.FleetConfig
|
||||
clock clock.Clock
|
||||
authz *authz.Authorizer
|
||||
depStorage storage.AllStorage
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@@ -27,6 +29,7 @@ func NewService(
|
||||
config config.FleetConfig,
|
||||
mailService fleet.MailService,
|
||||
c clock.Clock,
|
||||
depStorage storage.AllStorage,
|
||||
) (*Service, error) {
|
||||
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
@@ -35,12 +38,13 @@ func NewService(
|
||||
}
|
||||
|
||||
eeservice := &Service{
|
||||
Service: svc,
|
||||
ds: ds,
|
||||
logger: logger,
|
||||
config: config,
|
||||
clock: c,
|
||||
authz: authorizer,
|
||||
Service: svc,
|
||||
ds: ds,
|
||||
logger: logger,
|
||||
config: config,
|
||||
clock: c,
|
||||
authz: authorizer,
|
||||
depStorage: depStorage,
|
||||
}
|
||||
|
||||
// Override methods that can't be easily overriden via
|
||||
|
||||
@@ -12,3 +12,15 @@ type AppleMDM struct {
|
||||
func (a AppleMDM) AuthzType() string {
|
||||
return "mdm_apple"
|
||||
}
|
||||
|
||||
type AppleBM struct {
|
||||
AppleID string `json:"apple_id"`
|
||||
OrgName string `json:"org_name"`
|
||||
MDMServerURL string `json:"mdm_server_url"`
|
||||
RenewDate time.Time `json:"renew_date"`
|
||||
DefaultTeam string `json:"default_team"`
|
||||
}
|
||||
|
||||
func (a AppleBM) AuthzType() string {
|
||||
return "mdm_apple"
|
||||
}
|
||||
|
||||
@@ -529,6 +529,7 @@ type Service interface {
|
||||
// Apple MDM
|
||||
|
||||
GetAppleMDM(ctx context.Context) (*AppleMDM, error)
|
||||
GetAppleBM(ctx context.Context) (*AppleBM, error)
|
||||
|
||||
// NewMDMAppleEnrollmentProfile creates and returns new enrollment profile.
|
||||
// Such enrollment profiles allow devices to enroll to Fleet MDM.
|
||||
|
||||
@@ -9,3 +9,11 @@ func (c *Client) GetAppleMDM() (*fleet.AppleMDM, error) {
|
||||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "")
|
||||
return responseBody.AppleMDM, err
|
||||
}
|
||||
|
||||
// GetAppleBM retrieves the Apple Business Manager information.
|
||||
func (c *Client) GetAppleBM() (*fleet.AppleBM, error) {
|
||||
verb, path := "GET", "/api/latest/fleet/mdm/apple_bm"
|
||||
var responseBody getAppleBMResponse
|
||||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "")
|
||||
return responseBody.AppleBM, err
|
||||
}
|
||||
|
||||
@@ -420,6 +420,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
ue.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
|
||||
}
|
||||
ue.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
|
||||
ue.GET("/api/_version_/fleet/mdm/apple_bm", getAppleBMEndpoint, nil)
|
||||
|
||||
errorLimiter := ratelimit.NewErrorMiddleware(limitStore)
|
||||
|
||||
|
||||
@@ -4265,7 +4265,7 @@ func (s *integrationTestSuite) TestPacksBadRequests() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestTeamsEndpointsWithoutLicense() {
|
||||
func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
|
||||
t := s.T()
|
||||
|
||||
// list teams, none
|
||||
@@ -4321,6 +4321,11 @@ func (s *integrationTestSuite) TestTeamsEndpointsWithoutLicense() {
|
||||
// modify team enroll secrets
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/teams/123/secrets", modifyTeamEnrollSecretsRequest{Secrets: []fleet.EnrollSecret{{Secret: "DEF"}}}, http.StatusPaymentRequired, &secResp)
|
||||
assert.Len(t, secResp.Secrets, 0)
|
||||
|
||||
// get apple BM configuration
|
||||
var appleBMResp getAppleBMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusPaymentRequired, &appleBMResp)
|
||||
assert.Nil(t, appleBMResp.AppleBM)
|
||||
}
|
||||
|
||||
// TestGlobalPoliciesBrowsing tests that team users can browse (read) global policies (see #3722).
|
||||
|
||||
@@ -1653,6 +1653,13 @@ func (s *integrationEnterpriseTestSuite) TestListHosts() {
|
||||
require.Nil(t, summaryResp.LowDiskSpaceCount)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestAppleMDMNotConfigured() {
|
||||
var mdmResp getAppleMDMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusNotFound, &mdmResp)
|
||||
var bmResp getAppleBMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusNotFound, &bmResp)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
|
||||
fields := []string{"Query", "Name", "Description", "Resolution", "Platform", "Critical"}
|
||||
|
||||
|
||||
@@ -48,3 +48,27 @@ func (svc *Service) GetAppleMDM(ctx context.Context) (*fleet.AppleMDM, error) {
|
||||
|
||||
return appleMDM, nil
|
||||
}
|
||||
|
||||
type getAppleBMResponse struct {
|
||||
*fleet.AppleBM
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r getAppleBMResponse) error() error { return r.Err }
|
||||
|
||||
func getAppleBMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
||||
appleBM, err := svc.GetAppleBM(ctx)
|
||||
if err != nil {
|
||||
return getAppleBMResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return getAppleBMResponse{AppleBM: appleBM}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
||||
panic(err)
|
||||
}
|
||||
if lic.IsPremium() {
|
||||
svc, err = eeservice.NewService(svc, ds, kitlog.NewNopLogger(), fleetConfig, mailer, c)
|
||||
svc, err = eeservice.NewService(svc, ds, kitlog.NewNopLogger(), fleetConfig, mailer, c, depStorage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user