Implement fleetctl get mdm-apple (#8786)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added the `fleetctl get mdm_apple` command to retrieve the Apple MDM configuration information.
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fleetdm/fleet/v4/pkg/secure"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
|
||||
@@ -40,6 +42,23 @@ func defaultTable(writer io.Writer) *tablewriter.Table {
|
||||
return table
|
||||
}
|
||||
|
||||
func borderlessTabularTable(writer io.Writer) *tablewriter.Table {
|
||||
w := writerOrStdout(writer)
|
||||
table := tablewriter.NewWriter(w)
|
||||
table.SetRowLine(false)
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetHeaderLine(false)
|
||||
table.SetBorder(false)
|
||||
table.SetTablePadding("\t")
|
||||
table.SetNoWhiteSpace(true)
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
func writerOrStdout(writer io.Writer) io.Writer {
|
||||
var w io.Writer
|
||||
w = os.Stdout
|
||||
@@ -273,6 +292,7 @@ func getCommand() *cli.Command {
|
||||
getUserRolesCommand(),
|
||||
getTeamsCommand(),
|
||||
getSoftwareCommand(),
|
||||
getMDMAppleCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -883,6 +903,12 @@ func printTable(c *cli.Context, columns []string, data [][]string) {
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func printKeyValueTable(c *cli.Context, rows [][]string) {
|
||||
table := borderlessTabularTable(c.App.Writer)
|
||||
table.AppendBulk(rows)
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func getTeamsCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "teams",
|
||||
@@ -1022,3 +1048,53 @@ func getSoftwareCommand() *cli.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getMDMAppleCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "mdm_apple",
|
||||
Hidden: true, // TODO: temporary, until the MDM feature is officially released
|
||||
Aliases: []string{"mdm-apple"},
|
||||
Usage: "Show Apple Push Notification Service (APNs) information",
|
||||
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
|
||||
}
|
||||
|
||||
mdm, err := client.GetAppleMDM()
|
||||
if err != nil {
|
||||
var nfe service.NotFoundErr
|
||||
if errors.As(err, &nfe) {
|
||||
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
|
||||
}
|
||||
|
||||
printKeyValueTable(c, [][]string{
|
||||
{"Common name (CN):", mdm.CommonName},
|
||||
{"Serial number:", mdm.SerialNumber},
|
||||
{"Issuer:", mdm.Issuer},
|
||||
{"Renew date:", mdm.RenewDate.Format("January 2, 2006")},
|
||||
})
|
||||
|
||||
warnDate := time.Now().Add(expirationWarning)
|
||||
if mdm.RenewDate.Before(time.Now()) {
|
||||
// certificate is expired, print an error
|
||||
color.New(color.FgRed).Fprintln(writerOrStdout(c.App.Writer), "\nERROR: Your Apple Push Notification service (APNs) certificate is expired. MDM features are turned off. To renew your APNs certificate, follow these instructions: [TODO link to documentation]")
|
||||
} else if mdm.RenewDate.Before(warnDate) {
|
||||
// certificate will soon expire, print a warning
|
||||
color.New(color.FgYellow).Fprintln(writerOrStdout(c.App.Writer), "\nWARNING: Your Apple Push Notification service (APNs) certificate is less than 30 days from expiration. If it expires, MDM features will be turned off. To renew your APNs certificate, follow these instructions: [TODO link to documentation]")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1451,3 +1451,12 @@ func TestEnrichedAppConfig(t *testing.T) {
|
||||
require.Equal(t, "filesystem", enriched.Logging.Status.Plugin)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAppleMDM(t *testing.T) {
|
||||
runServerWithMockedDS(t)
|
||||
|
||||
// can only test when no MDM cert is provided, otherwise they would have to
|
||||
// be valid Apple APNs and SCEP certs.
|
||||
expected := `Error: No Apple Push Notification service (APNs) certificate found.`
|
||||
assert.Contains(t, runAppForTest(t, []string{"get", "mdm_apple"}), expected)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# API for contributors
|
||||
|
||||
- [Packs](#packs)
|
||||
- [Mobile device management (MDM) - IN PROGRESS](#mobile-device-management-mdm-in-progress)
|
||||
- [Get or apply configuration files](#get-or-apply-configuration-files)
|
||||
- [Live query](#live-query)
|
||||
- [Trigger cron schedule](#trigger-cron-schedule)
|
||||
- [Device-authenticated routes](#device-authenticated-routes)
|
||||
- [Downloadable installers](#downloadable-installers)
|
||||
- [Setup](#setup)
|
||||
@@ -516,6 +518,39 @@ Delete pack by name.
|
||||
|
||||
---
|
||||
|
||||
## Mobile device management (MDM) - IN PROGRESS
|
||||
|
||||
> This feature is currently in development and is not ready for use.
|
||||
|
||||
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 MDM
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple`
|
||||
|
||||
#### Parameters
|
||||
|
||||
None.
|
||||
|
||||
#### Example
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
{
|
||||
"common_name": "APSP:04u52i98aewuh-xxxx-xxxx-xxxx-xxxx",
|
||||
"serial_number": "1234567890987654321",
|
||||
"issuer": "Apple Application Integration 2 Certification Authority",
|
||||
"renew_date": "2023-09-30T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 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/).
|
||||
@@ -1965,7 +2000,7 @@ currently pending.
|
||||
|
||||
---
|
||||
|
||||
### Device-authenticated routes
|
||||
## Device-authenticated routes
|
||||
|
||||
Device-authenticated routes are routes used by the Fleet Desktop application. Unlike most other routes, Fleet user's API token does not authenticate them. They use a device-specific token.
|
||||
|
||||
@@ -2378,7 +2413,7 @@ If an installer with the provided parameters is found.
|
||||
|
||||
If an installer with the provided parameters doesn't exist.
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
Sets up a new Fleet instance with the given parameters.
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ Users with the Admin role receive all permissions.
|
||||
| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | ✅ |
|
||||
| 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 | | | ✅ |
|
||||
|
||||
|
||||
|
||||
@@ -52,7 +53,7 @@ Users with the Admin role receive all permissions.
|
||||
|
||||
`Applies only to Fleet Premium`
|
||||
|
||||
Users in Fleet either have team access or global access.
|
||||
Users in Fleet either have team access or global access.
|
||||
|
||||
Users with team access only have access to the [hosts](https://fleetdm.com/docs/using-fleet/rest-api#hosts), [software](https://fleetdm.com/docs/using-fleet/rest-api#software), [schedules](https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query) , and [policies](https://fleetdm.com/docs/using-fleet/rest-api#policies) assigned to
|
||||
their team.
|
||||
|
||||
@@ -515,6 +515,13 @@ allow {
|
||||
# Apple MDM
|
||||
##
|
||||
|
||||
# Global admins can read and write MDM apple information.
|
||||
allow {
|
||||
object.type == "mdm_apple"
|
||||
subject.global_role == admin
|
||||
action == [read, write][_]
|
||||
}
|
||||
|
||||
# Global admins can read and write Apple MDM enrollments.
|
||||
allow {
|
||||
object.type == "mdm_apple_enrollment_profile"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package fleet
|
||||
|
||||
import "time"
|
||||
|
||||
type AppleMDM struct {
|
||||
CommonName string `json:"common_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Issuer string `json:"issuer"`
|
||||
RenewDate time.Time `json:"renew_date"`
|
||||
}
|
||||
|
||||
func (a AppleMDM) AuthzType() string {
|
||||
return "mdm_apple"
|
||||
}
|
||||
@@ -528,6 +528,8 @@ type Service interface {
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Apple MDM
|
||||
|
||||
GetAppleMDM(ctx context.Context) (*AppleMDM, error)
|
||||
|
||||
// NewMDMAppleEnrollmentProfile creates and returns new enrollment profile.
|
||||
// Such enrollment profiles allow devices to enroll to Fleet MDM.
|
||||
NewMDMAppleEnrollmentProfile(ctx context.Context, enrollmentPayload MDMAppleEnrollmentProfilePayload) (enrollmentProfile *MDMAppleEnrollmentProfile, err error)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package service
|
||||
|
||||
import "github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
||||
// GetAppleMDM retrieves the Apple MDM APNs information.
|
||||
func (c *Client) GetAppleMDM() (*fleet.AppleMDM, error) {
|
||||
verb, path := "GET", "/api/latest/fleet/mdm/apple"
|
||||
var responseBody getAppleMDMResponse
|
||||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "")
|
||||
return responseBody.AppleMDM, err
|
||||
}
|
||||
@@ -419,6 +419,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
ue.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
|
||||
ue.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
|
||||
}
|
||||
ue.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
|
||||
|
||||
errorLimiter := ratelimit.NewErrorMiddleware(limitStore)
|
||||
|
||||
|
||||
@@ -5868,6 +5868,11 @@ func (s *integrationTestSuite) TestPingEndpoints() {
|
||||
s.DoRawNoAuth("HEAD", "/api/fleet/device/ping", nil, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestAppleMDMNotConfigured() {
|
||||
var resp getAppleMDMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusNotFound, &resp)
|
||||
}
|
||||
|
||||
// this test can be deleted once the "v1" version is removed.
|
||||
func (s *integrationTestSuite) TestAPIVersion_v1_2022_04() {
|
||||
t := s.T()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
type getAppleMDMResponse struct {
|
||||
*fleet.AppleMDM
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r getAppleMDMResponse) error() error { return r.Err }
|
||||
|
||||
func getAppleMDMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
||||
appleMDM, err := svc.GetAppleMDM(ctx)
|
||||
if err != nil {
|
||||
return getAppleMDMResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return getAppleMDMResponse{AppleMDM: appleMDM}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetAppleMDM(ctx context.Context) (*fleet.AppleMDM, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleMDM{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if there is no apple mdm config, fail with a 404
|
||||
if !svc.config.MDM.IsAppleAPNsSet() {
|
||||
return nil, notFoundError{}
|
||||
}
|
||||
|
||||
apns, _, _, err := svc.config.MDM.AppleAPNs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appleMDM := &fleet.AppleMDM{
|
||||
CommonName: apns.Leaf.Subject.CommonName,
|
||||
Issuer: apns.Leaf.Issuer.CommonName,
|
||||
RenewDate: apns.Leaf.NotAfter,
|
||||
}
|
||||
if apns.Leaf.SerialNumber != nil {
|
||||
appleMDM.SerialNumber = apns.Leaf.SerialNumber.String()
|
||||
}
|
||||
|
||||
return appleMDM, nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetMDMApple(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
|
||||
cfg := config.TestConfig()
|
||||
cfg.MDM.AppleAPNsCert = "testdata/server.pem"
|
||||
cfg.MDM.AppleAPNsKey = "testdata/server.key"
|
||||
cfg.MDM.AppleSCEPCert = "testdata/server.pem"
|
||||
cfg.MDM.AppleSCEPKey = "testdata/server.key"
|
||||
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
||||
|
||||
_, _, _, err := cfg.MDM.AppleAPNs()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = test.UserContext(ctx, test.UserAdmin)
|
||||
got, err := svc.GetAppleMDM(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// NOTE: to inspect the test certificate, you can use:
|
||||
// openssl x509 -in ./server/service/testdata/server.pem -text -noout
|
||||
require.Equal(t, &fleet.AppleMDM{
|
||||
CommonName: "servq.groob.io",
|
||||
SerialNumber: "1",
|
||||
Issuer: "groob-ca",
|
||||
RenewDate: time.Date(2017, 10, 24, 13, 11, 44, 0, time.UTC),
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestMDMAppleAuthorization(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true})
|
||||
|
||||
// use a custom implementation of checkAuthErr as the service call will fail
|
||||
// with a not found error (given that MDM is not really configured) in case
|
||||
// of success, and the package-wide checkAuthErr requires no error.
|
||||
checkAuthErr := func(t *testing.T, shouldFail bool, err error) {
|
||||
if shouldFail {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
|
||||
} else if err != nil {
|
||||
require.NotEqual(t, (&authz.Forbidden{}).Error(), err.Error())
|
||||
}
|
||||
}
|
||||
testAuthdMethods := func(t *testing.T, user *fleet.User, shouldFailWithAuth bool) {
|
||||
ctx := test.UserContext(ctx, user)
|
||||
_, err := svc.GetAppleMDM(ctx)
|
||||
checkAuthErr(t, shouldFailWithAuth, err)
|
||||
}
|
||||
|
||||
// Only global admins can access the endpoints.
|
||||
testAuthdMethods(t, test.UserAdmin, false)
|
||||
|
||||
// All other users should not have access to the endpoints.
|
||||
for _, user := range []*fleet.User{
|
||||
test.UserNoRoles,
|
||||
test.UserMaintainer,
|
||||
test.UserObserver,
|
||||
test.UserTeamAdminTeam1,
|
||||
} {
|
||||
testAuthdMethods(t, user, true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user