Implement fleetctl get mdm-apple (#8786)

This commit is contained in:
Martin Angers
2022-12-05 11:35:45 -05:00
committed by GitHub
parent f5fc316ad3
commit 866829dbfa
13 changed files with 291 additions and 3 deletions
@@ -0,0 +1 @@
* Added the `fleetctl get mdm_apple` command to retrieve the Apple MDM configuration information.
+76
View File
@@ -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
},
}
}
+9
View File
@@ -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)
}
+37 -2
View File
@@ -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.
+2 -1
View File
@@ -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.
+7
View File
@@ -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"
+14
View File
@@ -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"
}
+2
View File
@@ -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)
+11
View File
@@ -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
}
+1
View File
@@ -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)
+5
View File
@@ -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()
+50
View File
@@ -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
}
+76
View File
@@ -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)
}
}