mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
4a1f3988f0
#9587 and #9639
1081 lines
38 KiB
Go
1081 lines
38 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
mathrand "math/rand"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/micromdm/nanomdm/mdm"
|
|
"github.com/micromdm/nanomdm/push"
|
|
nanomdm_pushsvc "github.com/micromdm/nanomdm/push/service"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service/mock"
|
|
"github.com/fleetdm/fleet/v4/server/service/schedule"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
kitlog "github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
"github.com/google/uuid"
|
|
"github.com/groob/plist"
|
|
"github.com/jmoiron/sqlx"
|
|
nanodep_client "github.com/micromdm/nanodep/client"
|
|
"github.com/micromdm/nanodep/godep"
|
|
nanodep_storage "github.com/micromdm/nanodep/storage"
|
|
"github.com/micromdm/nanodep/tokenpki"
|
|
scepclient "github.com/micromdm/scep/v2/client"
|
|
"github.com/micromdm/scep/v2/cryptoutil/x509util"
|
|
"github.com/micromdm/scep/v2/scep"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
"go.mozilla.org/pkcs7"
|
|
)
|
|
|
|
func TestIntegrationsMDM(t *testing.T) {
|
|
testingSuite := new(integrationMDMTestSuite)
|
|
testingSuite.s = &testingSuite.Suite
|
|
suite.Run(t, testingSuite)
|
|
}
|
|
|
|
type integrationMDMTestSuite struct {
|
|
suite.Suite
|
|
withServer
|
|
fleetCfg config.FleetConfig
|
|
fleetDMNextCSRStatus atomic.Value
|
|
pushProvider *mock.APNSPushProvider
|
|
depStorage nanodep_storage.AllStorage
|
|
depSchedule *schedule.Schedule
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) SetupSuite() {
|
|
s.withDS.SetupSuite("integrationMDMTestSuite")
|
|
|
|
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
|
|
require.NoError(s.T(), err)
|
|
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
|
|
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
|
|
|
|
fleetCfg := config.TestConfig()
|
|
config.SetTestMDMConfig(s.T(), &fleetCfg, testCertPEM, testKeyPEM, testBMToken)
|
|
|
|
mdmStorage, err := s.ds.NewMDMAppleMDMStorage(testCertPEM, testKeyPEM)
|
|
require.NoError(s.T(), err)
|
|
depStorage, err := s.ds.NewMDMAppleDEPStorage(*testBMToken)
|
|
require.NoError(s.T(), err)
|
|
scepStorage, err := s.ds.NewSCEPDepot(testCertPEM, testKeyPEM)
|
|
require.NoError(s.T(), err)
|
|
|
|
pushFactory, pushProvider := newMockAPNSPushProviderFactory()
|
|
mdmPushService := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)),
|
|
)
|
|
|
|
var depSchedule *schedule.Schedule
|
|
config := TestServerOpts{
|
|
License: &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
},
|
|
FleetConfig: &fleetCfg,
|
|
MDMStorage: mdmStorage,
|
|
DEPStorage: depStorage,
|
|
SCEPStorage: scepStorage,
|
|
MDMPusher: mdmPushService,
|
|
StartCronSchedules: []TestNewScheduleFunc{
|
|
func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
|
|
return func() (fleet.CronSchedule, error) {
|
|
const name = string(fleet.CronAppleMDMDEPProfileAssigner)
|
|
logger := kitlog.NewJSONLogger(os.Stdout)
|
|
fleetSyncer := apple_mdm.NewDEPSyncer(ds, depStorage, logger, true)
|
|
depSchedule = schedule.New(
|
|
ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
|
|
schedule.WithLogger(logger),
|
|
schedule.WithJob("dep_syncer", func(ctx context.Context) error {
|
|
return fleetSyncer.Run(ctx)
|
|
}),
|
|
)
|
|
return depSchedule, nil
|
|
}
|
|
},
|
|
},
|
|
}
|
|
users, server := RunServerForTestsWithDS(s.T(), s.ds, &config)
|
|
s.server = server
|
|
s.users = users
|
|
s.token = s.getTestAdminToken()
|
|
s.cachedAdminToken = s.token
|
|
s.fleetCfg = fleetCfg
|
|
s.pushProvider = pushProvider
|
|
s.depStorage = depStorage
|
|
s.depSchedule = depSchedule
|
|
|
|
fleetdmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
status := s.fleetDMNextCSRStatus.Swap(http.StatusOK)
|
|
w.WriteHeader(status.(int))
|
|
_, _ = w.Write([]byte(fmt.Sprintf("status: %d", status)))
|
|
}))
|
|
s.T().Setenv("TEST_FLEETDM_API_URL", fleetdmSrv.URL)
|
|
s.T().Cleanup(fleetdmSrv.Close)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) FailNextCSRRequestWith(status int) {
|
|
s.fleetDMNextCSRStatus.Store(status)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) SucceedNextCSRRequest() {
|
|
s.fleetDMNextCSRStatus.Store(http.StatusOK)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TearDownTest() {
|
|
s.withServer.commonTearDownTest(s.T())
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) mockDEPResponse(handler http.Handler) {
|
|
t := s.T()
|
|
srv := httptest.NewServer(handler)
|
|
err := s.depStorage.StoreConfig(context.Background(), apple_mdm.DEPName, &nanodep_client.Config{BaseURL: srv.URL})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
srv.Close()
|
|
err := s.depStorage.StoreConfig(context.Background(), apple_mdm.DEPName, &nanodep_client.Config{BaseURL: nanodep_client.DefaultBaseURL})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
|
|
t := s.T()
|
|
|
|
var mdmResp getAppleMDMResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusOK, &mdmResp)
|
|
// returned values are dummy, this is a test certificate
|
|
require.Equal(t, "FleetDM", mdmResp.Issuer)
|
|
require.NotZero(t, mdmResp.SerialNumber)
|
|
require.Equal(t, "FleetDM", mdmResp.CommonName)
|
|
require.NotZero(t, mdmResp.RenewDate)
|
|
|
|
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
switch r.URL.Path {
|
|
case "/session":
|
|
_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
|
|
case "/account":
|
|
_, _ = w.Write([]byte(`{"admin_id": "abc", "org_name": "test_org"}`))
|
|
}
|
|
}))
|
|
var getAppleBMResp getAppleBMResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusOK, &getAppleBMResp)
|
|
require.NoError(t, getAppleBMResp.Err)
|
|
require.Equal(t, "abc", getAppleBMResp.AppleID)
|
|
require.Equal(t, "test_org", getAppleBMResp.OrgName)
|
|
require.Equal(t, "https://example.org/mdm/apple/mdm", getAppleBMResp.MDMServerURL)
|
|
require.Empty(t, getAppleBMResp.DefaultTeam)
|
|
|
|
// create a new team
|
|
tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
|
|
Name: t.Name(),
|
|
Description: "desc",
|
|
})
|
|
require.NoError(t, err)
|
|
// set the default bm assignment to that team
|
|
acResp := appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
|
|
"mdm": {
|
|
"apple_bm_default_team": %q
|
|
}
|
|
}`, tm.Name)), http.StatusOK, &acResp)
|
|
|
|
// try again, this time we get a default team in the response
|
|
getAppleBMResp = getAppleBMResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusOK, &getAppleBMResp)
|
|
require.NoError(t, getAppleBMResp.Err)
|
|
require.Equal(t, "abc", getAppleBMResp.AppleID)
|
|
require.Equal(t, "test_org", getAppleBMResp.OrgName)
|
|
require.Equal(t, "https://example.org/mdm/apple/mdm", getAppleBMResp.MDMServerURL)
|
|
require.Equal(t, tm.Name, getAppleBMResp.DefaultTeam)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestABMExpiredToken() {
|
|
t := s.T()
|
|
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
_, _ = w.Write([]byte(`{"code": "T_C_NOT_SIGNED"}`))
|
|
}))
|
|
|
|
config := s.getConfig()
|
|
require.False(t, config.MDM.AppleBMTermsExpired)
|
|
|
|
var getAppleBMResp getAppleBMResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusInternalServerError, &getAppleBMResp)
|
|
|
|
config = s.getConfig()
|
|
require.True(t, config.MDM.AppleBMTermsExpired)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
|
|
t := s.T()
|
|
devices := []godep.Device{
|
|
{SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
|
|
{SerialNumber: uuid.New().String(), Model: "MacBook Mini", OS: "osx", OpType: "added"},
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
encoder := json.NewEncoder(w)
|
|
switch r.URL.Path {
|
|
case "/session":
|
|
err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
|
|
require.NoError(t, err)
|
|
case "/profile":
|
|
err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
|
|
require.NoError(t, err)
|
|
case "/server/devices":
|
|
// This endpoint is used to get an initial list of
|
|
// devices, return a single device
|
|
err := encoder.Encode(godep.DeviceResponse{Devices: devices[:1]})
|
|
require.NoError(t, err)
|
|
case "/devices/sync":
|
|
// This endpoint is polled over time to sync devices from
|
|
// ABM, send a repeated serial and a new one
|
|
err := encoder.Encode(godep.DeviceResponse{Devices: devices})
|
|
require.NoError(t, err)
|
|
case "/profile/devices":
|
|
wg.Done()
|
|
_, _ = w.Write([]byte(`{}`))
|
|
default:
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}
|
|
}))
|
|
|
|
// create a DEP enrollment profile
|
|
profile := json.RawMessage("{}")
|
|
var createProfileResp createMDMAppleEnrollmentProfileResponse
|
|
createProfileReq := createMDMAppleEnrollmentProfileRequest{
|
|
Type: "automatic",
|
|
DEPProfile: &profile,
|
|
}
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/enrollmentprofiles", createProfileReq, http.StatusOK, &createProfileResp)
|
|
|
|
// query all hosts
|
|
listHostsRes := listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Empty(t, listHostsRes.Hosts)
|
|
|
|
// trigger a profile sync
|
|
_, err := s.depSchedule.Trigger()
|
|
require.NoError(t, err)
|
|
wg.Wait()
|
|
|
|
// both hosts should be returned from the hosts endpoint
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(t, listHostsRes.Hosts, 2)
|
|
require.Equal(t, listHostsRes.Hosts[0].HardwareSerial, devices[0].SerialNumber)
|
|
require.Equal(t, listHostsRes.Hosts[1].HardwareSerial, devices[1].SerialNumber)
|
|
require.EqualValues(
|
|
t,
|
|
[]string{devices[0].SerialNumber, devices[1].SerialNumber},
|
|
[]string{listHostsRes.Hosts[0].HardwareSerial, listHostsRes.Hosts[1].HardwareSerial},
|
|
)
|
|
|
|
// create a new host
|
|
createHostAndDeviceToken(t, s.ds, "not-dep")
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(t, listHostsRes.Hosts, 3)
|
|
|
|
// filtering by MDM status works
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts?mdm_enrollment_status=pending", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(t, listHostsRes.Hosts, 2)
|
|
|
|
// enroll one of the hosts
|
|
d := newDevice(s)
|
|
d.serial = devices[0].SerialNumber
|
|
d.mdmEnroll(s)
|
|
|
|
// only one shows up as pending
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts?mdm_enrollment_status=pending", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(t, listHostsRes.Hosts, 1)
|
|
|
|
activities := listActivitiesResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities, "order_key", "created_at")
|
|
found := false
|
|
for _, activity := range activities.Activities {
|
|
if activity.Type == "mdm_enrolled" &&
|
|
strings.Contains(string(*activity.Details), devices[0].SerialNumber) {
|
|
found = true
|
|
require.Nil(t, activity.ActorID)
|
|
require.Nil(t, activity.ActorFullName)
|
|
require.JSONEq(
|
|
t,
|
|
fmt.Sprintf(
|
|
`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": true}`,
|
|
devices[0].SerialNumber, devices[0].Model, devices[0].SerialNumber,
|
|
),
|
|
string(*activity.Details),
|
|
)
|
|
}
|
|
}
|
|
require.True(t, found)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestDeviceMDMManualEnroll() {
|
|
t := s.T()
|
|
|
|
token := "token_test_manual_enroll"
|
|
createHostAndDeviceToken(t, s.ds, token)
|
|
|
|
// invalid token fails
|
|
s.DoRaw("GET", "/api/latest/fleet/device/invalid_token/mdm/apple/manual_enrollment_profile", nil, http.StatusUnauthorized)
|
|
|
|
// valid token downloads the profile
|
|
resp := s.DoRaw("GET", "/api/latest/fleet/device/"+token+"/mdm/apple/manual_enrollment_profile", nil, http.StatusOK)
|
|
body, err := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
require.NoError(t, err)
|
|
require.Contains(t, resp.Header, "Content-Disposition")
|
|
require.Contains(t, resp.Header, "Content-Type")
|
|
require.Contains(t, resp.Header, "X-Content-Type-Options")
|
|
require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
|
|
require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
|
|
require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
|
|
headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(body), headerLen)
|
|
|
|
var profile struct {
|
|
PayloadIdentifier string `plist:"PayloadIdentifier"`
|
|
}
|
|
require.NoError(t, plist.Unmarshal(body, &profile))
|
|
require.Equal(t, apple_mdm.FleetPayloadIdentifier, profile.PayloadIdentifier)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
|
t := s.T()
|
|
|
|
// Enroll two devices into MDM
|
|
deviceA := newMDMEnrolledDevice(s)
|
|
deviceB := newMDMEnrolledDevice(s)
|
|
|
|
// Find the ID of Fleet's MDM solution
|
|
var mdmID uint
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(context.Background(), q, &mdmID,
|
|
`SELECT id FROM mobile_device_management_solutions WHERE name = ?`, fleet.WellKnownMDMFleet)
|
|
})
|
|
|
|
// Check that both devices are returned by the /hosts endpoint
|
|
listHostsRes := listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes, "mdm_id", fmt.Sprint(mdmID))
|
|
require.Len(t, listHostsRes.Hosts, 2)
|
|
require.EqualValues(
|
|
t,
|
|
[]string{deviceA.uuid, deviceB.uuid},
|
|
[]string{listHostsRes.Hosts[0].UUID, listHostsRes.Hosts[1].UUID},
|
|
)
|
|
|
|
var targetHostID uint
|
|
var lastEnroll time.Time
|
|
for _, host := range listHostsRes.Hosts {
|
|
if host.UUID == deviceA.uuid {
|
|
targetHostID = host.ID
|
|
lastEnroll = host.LastEnrolledAt
|
|
break
|
|
}
|
|
}
|
|
|
|
// Activities are generated for each device
|
|
activities := listActivitiesResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities, "order_key", "created_at")
|
|
require.GreaterOrEqual(t, len(activities.Activities), 2)
|
|
|
|
details := []*json.RawMessage{}
|
|
for _, activity := range activities.Activities {
|
|
if activity.Type == "mdm_enrolled" {
|
|
require.Nil(t, activity.ActorID)
|
|
require.Nil(t, activity.ActorFullName)
|
|
details = append(details, activity.Details)
|
|
}
|
|
}
|
|
require.Len(t, details, 2)
|
|
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, deviceA.serial, deviceA.model, deviceA.serial), string(*details[len(details)-2]))
|
|
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, deviceB.serial, deviceB.model, deviceB.serial), string(*details[len(details)-1]))
|
|
|
|
// set an enroll secret
|
|
var applyResp applyEnrollSecretSpecResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
|
Spec: &fleet.EnrollSecretSpec{
|
|
Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
|
|
},
|
|
}, http.StatusOK, &applyResp)
|
|
|
|
// simulate a matching host enrolling via osquery
|
|
j, err := json.Marshal(&enrollAgentRequest{
|
|
EnrollSecret: t.Name(),
|
|
HostIdentifier: deviceA.uuid,
|
|
})
|
|
require.NoError(t, err)
|
|
var enrollResp enrollAgentResponse
|
|
hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK)
|
|
defer hres.Body.Close()
|
|
require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp))
|
|
require.NotEmpty(t, enrollResp.NodeKey)
|
|
|
|
// query all hosts
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
// we still have only two hosts
|
|
require.Len(t, listHostsRes.Hosts, 2)
|
|
|
|
// LastEnrolledAt should have been updated
|
|
var getHostResp getHostResponse
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", targetHostID), nil, http.StatusOK, &getHostResp)
|
|
require.Greater(t, getHostResp.Host.LastEnrolledAt, lastEnroll)
|
|
|
|
// Unenroll a device
|
|
deviceA.checkout()
|
|
|
|
// An activity is created
|
|
activities = listActivitiesResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
|
|
|
|
found := false
|
|
for _, activity := range activities.Activities {
|
|
if activity.Type == "mdm_unenrolled" {
|
|
found = true
|
|
require.Nil(t, activity.ActorID)
|
|
require.Nil(t, activity.ActorFullName)
|
|
details = append(details, activity.Details)
|
|
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, deviceA.serial, deviceA.model, deviceA.serial), string(*activity.Details))
|
|
}
|
|
}
|
|
require.True(t, found)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestDeviceMultipleAuthMessages() {
|
|
d := newMDMEnrolledDevice(s)
|
|
|
|
listHostsRes := listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(s.T(), listHostsRes.Hosts, 1)
|
|
|
|
// send the auth message again, we still have only one host
|
|
d.authenticate()
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(s.T(), listHostsRes.Hosts, 1)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestAppleMDMCSRRequest() {
|
|
t := s.T()
|
|
|
|
var errResp validationErrResp
|
|
// missing arguments
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{}, http.StatusUnprocessableEntity, &errResp)
|
|
require.Len(t, errResp.Errors, 1)
|
|
require.Equal(t, errResp.Errors[0].Name, "email_address")
|
|
|
|
// invalid email address
|
|
errResp = validationErrResp{}
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "abc", Organization: "def"}, http.StatusUnprocessableEntity, &errResp)
|
|
require.Len(t, errResp.Errors, 1)
|
|
require.Equal(t, errResp.Errors[0].Name, "email_address")
|
|
|
|
// missing organization
|
|
errResp = validationErrResp{}
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: ""}, http.StatusUnprocessableEntity, &errResp)
|
|
require.Len(t, errResp.Errors, 1)
|
|
require.Equal(t, errResp.Errors[0].Name, "organization")
|
|
|
|
// fleetdm CSR request failed
|
|
s.FailNextCSRRequestWith(http.StatusBadRequest)
|
|
errResp = validationErrResp{}
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusUnprocessableEntity, &errResp)
|
|
require.Len(t, errResp.Errors, 1)
|
|
require.Contains(t, errResp.Errors[0].Reason, "this email address is not valid")
|
|
|
|
s.FailNextCSRRequestWith(http.StatusInternalServerError)
|
|
errResp = validationErrResp{}
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusBadGateway, &errResp)
|
|
require.Len(t, errResp.Errors, 1)
|
|
require.Contains(t, errResp.Errors[0].Reason, "FleetDM CSR request failed")
|
|
|
|
var reqCSRResp requestMDMAppleCSRResponse
|
|
// fleetdm CSR request succeeds
|
|
s.SucceedNextCSRRequest()
|
|
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusOK, &reqCSRResp)
|
|
require.Contains(t, string(reqCSRResp.APNsKey), "-----BEGIN RSA PRIVATE KEY-----\n")
|
|
require.Contains(t, string(reqCSRResp.SCEPCert), "-----BEGIN CERTIFICATE-----\n")
|
|
require.Contains(t, string(reqCSRResp.SCEPKey), "-----BEGIN RSA PRIVATE KEY-----\n")
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
|
|
t := s.T()
|
|
// enroll into mdm
|
|
d := newMDMEnrolledDevice(s)
|
|
|
|
// set an enroll secret
|
|
var applyResp applyEnrollSecretSpecResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
|
Spec: &fleet.EnrollSecretSpec{
|
|
Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
|
|
},
|
|
}, http.StatusOK, &applyResp)
|
|
|
|
// simulate a matching host enrolling via osquery
|
|
j, err := json.Marshal(&enrollAgentRequest{
|
|
EnrollSecret: t.Name(),
|
|
HostIdentifier: d.uuid,
|
|
})
|
|
require.NoError(t, err)
|
|
var enrollResp enrollAgentResponse
|
|
hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK)
|
|
defer hres.Body.Close()
|
|
require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp))
|
|
require.NotEmpty(t, enrollResp.NodeKey)
|
|
|
|
listHostsRes := listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(t, listHostsRes.Hosts, 1)
|
|
h := listHostsRes.Hosts[0]
|
|
|
|
// try to unenroll the host, fails since the host doesn't respond
|
|
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusGatewayTimeout)
|
|
|
|
// we're going to modify this mock, make sure we restore its default
|
|
originalPushMock := s.pushProvider.PushFunc
|
|
defer func() { s.pushProvider.PushFunc = originalPushMock }()
|
|
|
|
// TODO: this is not working as expected, we're still waiting on the
|
|
// device to unenroll even if we weren't able to send a push
|
|
// notification, we should return an error instead.
|
|
//
|
|
// the APNs service returns an error
|
|
// s.pushProvider.PushFunc = mockFailedPush
|
|
// s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusOK)
|
|
|
|
// try again, but this time the host is online and answers
|
|
s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
|
|
res, err := mockSuccessfulPush(pushes)
|
|
d.checkout()
|
|
return res, err
|
|
}
|
|
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusOK)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() {
|
|
t := s.T()
|
|
ctx := context.Background()
|
|
|
|
// create a host
|
|
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now().Add(-1 * time.Minute),
|
|
OsqueryHostID: ptr.String(t.Name()),
|
|
NodeKey: ptr.String(t.Name()),
|
|
UUID: uuid.New().String(),
|
|
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
|
|
Platform: "darwin",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// add an encryption key for the host
|
|
cert, _, _, err := s.fleetCfg.MDM.AppleSCEP()
|
|
require.NoError(t, err)
|
|
parsed, err := x509.ParseCertificate(cert.Certificate[0])
|
|
require.NoError(t, err)
|
|
recoveryKey := "AAA-BBB-CCC"
|
|
encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{parsed})
|
|
require.NoError(t, err)
|
|
base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey)
|
|
|
|
err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64EncryptedKey)
|
|
require.NoError(t, err)
|
|
|
|
// request with no token
|
|
res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusUnauthorized)
|
|
res.Body.Close()
|
|
|
|
// encryption key not processed yet
|
|
resp := getHostEncryptionKeyResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)
|
|
|
|
// unable to decrypt encryption key
|
|
err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, false, time.Now())
|
|
require.NoError(t, err)
|
|
resp = getHostEncryptionKeyResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)
|
|
|
|
// no activities created so far
|
|
activities := listActivitiesResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
|
|
found := false
|
|
for _, activity := range activities.Activities {
|
|
if activity.Type == "read_host_disk_encryption_key" {
|
|
found = true
|
|
}
|
|
}
|
|
require.False(t, found)
|
|
|
|
// decryptable key
|
|
checkDecryptableKey := func(u fleet.User) {
|
|
err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, time.Now())
|
|
require.NoError(t, err)
|
|
resp = getHostEncryptionKeyResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
|
|
require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)
|
|
|
|
// use the admin token to get the activities
|
|
currToken := s.token
|
|
defer func() { s.token = currToken }()
|
|
s.token = s.getTestAdminToken()
|
|
s.lastActivityMatches(
|
|
"read_host_disk_encryption_key",
|
|
fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID),
|
|
0,
|
|
)
|
|
}
|
|
|
|
// we're about to mess up with the token, make sure to set it to the
|
|
// default value when the test ends
|
|
currToken := s.token
|
|
t.Cleanup(func() { s.token = currToken })
|
|
|
|
// admins are able to see the host encryption key
|
|
s.token = s.getTestAdminToken()
|
|
checkDecryptableKey(s.users["admin1@example.com"])
|
|
|
|
// maintainers are able to see the token
|
|
u := s.users["user1@example.com"]
|
|
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
|
checkDecryptableKey(u)
|
|
|
|
// observers are able to see the token
|
|
u = s.users["user2@example.com"]
|
|
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
|
checkDecryptableKey(u)
|
|
|
|
// add the host to a team
|
|
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
|
|
ID: 4827,
|
|
Name: "team1_" + t.Name(),
|
|
Description: "desc team1_" + t.Name(),
|
|
})
|
|
require.NoError(t, err)
|
|
err = s.ds.AddHostsToTeam(ctx, &team.ID, []uint{host.ID})
|
|
require.NoError(t, err)
|
|
|
|
// admins are still able to see the token
|
|
s.token = s.getTestAdminToken()
|
|
checkDecryptableKey(s.users["admin1@example.com"])
|
|
|
|
// maintainers are still able to see the token
|
|
u = s.users["user1@example.com"]
|
|
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
|
checkDecryptableKey(u)
|
|
|
|
// observers are still able to see the token
|
|
u = s.users["user2@example.com"]
|
|
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
|
checkDecryptableKey(u)
|
|
|
|
// add a team member
|
|
u = fleet.User{
|
|
Name: "test team user",
|
|
Email: "user1+team@example.com",
|
|
GlobalRole: nil,
|
|
Teams: []fleet.UserTeam{
|
|
{
|
|
Team: *team,
|
|
Role: fleet.RoleMaintainer,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
|
|
_, err = s.ds.NewUser(ctx, &u)
|
|
require.NoError(t, err)
|
|
|
|
// members are able to see the token
|
|
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
|
checkDecryptableKey(u)
|
|
|
|
// create a separate team
|
|
team2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
|
|
ID: 4828,
|
|
Name: "team2_" + t.Name(),
|
|
Description: "desc team2_" + t.Name(),
|
|
})
|
|
require.NoError(t, err)
|
|
// add a team member
|
|
u = fleet.User{
|
|
Name: "test team user",
|
|
Email: "user1+team2@example.com",
|
|
GlobalRole: nil,
|
|
Teams: []fleet.UserTeam{
|
|
{
|
|
Team: *team2,
|
|
Role: fleet.RoleMaintainer,
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
|
|
_, err = s.ds.NewUser(ctx, &u)
|
|
require.NoError(t, err)
|
|
|
|
// non-members aren't able to see the token
|
|
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
|
resp = getHostEncryptionKeyResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusForbidden, &resp)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestAppConfigMDMAppleProfiles() {
|
|
t := s.T()
|
|
|
|
// set the macos custom settings fields
|
|
acResp := appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
|
"macos_settings": { "custom_settings": ["foo", "bar"] }
|
|
}`), http.StatusOK, &acResp)
|
|
assert.Equal(t, []string{"foo", "bar"}, acResp.MacOSSettings.CustomSettings)
|
|
|
|
// check that they are returned by a GET /config
|
|
acResp = appConfigResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
|
|
assert.Equal(t, []string{"foo", "bar"}, acResp.MacOSSettings.CustomSettings)
|
|
|
|
// patch without specifying the macos custom settings fields, should not remove them
|
|
acResp = appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
|
"macos_settings": {}
|
|
}`), http.StatusOK, &acResp)
|
|
assert.Equal(t, []string{"foo", "bar"}, acResp.MacOSSettings.CustomSettings)
|
|
|
|
// patch with explicitly empty macos custom settings fields, would remove
|
|
// them but this is a dry-run
|
|
acResp = appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
|
"macos_settings": {"custom_settings": null}
|
|
}`), http.StatusOK, &acResp, "dry_run", "true")
|
|
assert.Equal(t, []string{"foo", "bar"}, acResp.MacOSSettings.CustomSettings)
|
|
|
|
// patch with explicitly empty macos custom settings fields, removes them
|
|
acResp = appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
|
"macos_settings": {"custom_settings": null}
|
|
}`), http.StatusOK, &acResp)
|
|
assert.Empty(t, acResp.MacOSSettings.CustomSettings)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() {
|
|
t := s.T()
|
|
|
|
// create a team through the service so it initializes the agent ops
|
|
teamName := t.Name() + "team1"
|
|
team := &fleet.Team{
|
|
Name: teamName,
|
|
Description: "desc team1",
|
|
}
|
|
var createTeamResp teamResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
|
|
require.NotZero(t, createTeamResp.Team.ID)
|
|
team = createTeamResp.Team
|
|
|
|
// apply with custom macos settings
|
|
teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName,
|
|
MacOSSettings: map[string]interface{}{"custom_settings": []string{"foo", "bar"}}}}}
|
|
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
|
|
|
|
// retrieving the team returns the custom macos settings
|
|
var teamResp getTeamResponse
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
|
|
require.Equal(t, []string{"foo", "bar"}, teamResp.Team.Config.MacOSSettings.CustomSettings)
|
|
|
|
// apply with invalid macos settings subfield should fail
|
|
teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName,
|
|
MacOSSettings: map[string]interface{}{"foo_bar": 123}}}}
|
|
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusBadRequest)
|
|
|
|
// apply without custom macos settings specified, should not replace existing settings
|
|
teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName,
|
|
MacOSSettings: map[string]interface{}{}}}}
|
|
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
|
|
teamResp = getTeamResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
|
|
require.Equal(t, []string{"foo", "bar"}, teamResp.Team.Config.MacOSSettings.CustomSettings)
|
|
|
|
// apply with explicitly empty custom macos settings would clear the existing
|
|
// settings, but dry-run
|
|
teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName,
|
|
MacOSSettings: map[string]interface{}{"custom_settings": []string{}}}}}
|
|
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, "dry_run", "true")
|
|
teamResp = getTeamResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
|
|
require.Equal(t, []string{"foo", "bar"}, teamResp.Team.Config.MacOSSettings.CustomSettings)
|
|
|
|
// apply with explicitly empty custom macos settings clears the existing settings
|
|
teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName,
|
|
MacOSSettings: map[string]interface{}{"custom_settings": []string{}}}}}
|
|
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
|
|
teamResp = getTeamResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
|
|
require.Equal(t, []string{}, teamResp.Team.Config.MacOSSettings.CustomSettings)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestBatchSetMDMAppleProfiles() {
|
|
t := s.T()
|
|
ctx := context.Background()
|
|
|
|
// create a new team
|
|
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"})
|
|
require.NoError(t, err)
|
|
|
|
// apply an empty set to no-team
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil}, http.StatusNoContent)
|
|
s.lastActivityMatches(
|
|
fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
|
|
`{"team_id": null, "team_name": null}`,
|
|
0,
|
|
)
|
|
|
|
// apply to both team id and name
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil},
|
|
http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)), "team_name", tm.Name)
|
|
|
|
// invalid team name
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil},
|
|
http.StatusNotFound, "team_name", uuid.New().String())
|
|
|
|
// duplicate profile names
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
|
|
mobileconfigForTest("N1", "I1"),
|
|
mobileconfigForTest("N1", "I2"),
|
|
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
|
|
|
|
// successfully apply a profile for the team
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
|
|
mobileconfigForTest("N1", "I1"),
|
|
}}, http.StatusNoContent, "team_id", strconv.Itoa(int(tm.ID)))
|
|
s.lastActivityMatches(
|
|
fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
|
|
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
|
|
0,
|
|
)
|
|
}
|
|
|
|
type device struct {
|
|
uuid string
|
|
serial string
|
|
model string
|
|
|
|
s *integrationMDMTestSuite
|
|
scepCert *x509.Certificate
|
|
scepKey *rsa.PrivateKey
|
|
}
|
|
|
|
func newDevice(s *integrationMDMTestSuite) *device {
|
|
return &device{
|
|
uuid: strings.ToUpper(uuid.New().String()),
|
|
serial: randSerial(),
|
|
model: "MacBookPro16,1",
|
|
s: s,
|
|
}
|
|
}
|
|
|
|
func newMDMEnrolledDevice(s *integrationMDMTestSuite) *device {
|
|
d := newDevice(s)
|
|
d.mdmEnroll(s)
|
|
return d
|
|
}
|
|
|
|
func (d *device) mdmEnroll(s *integrationMDMTestSuite) {
|
|
d.scepEnroll()
|
|
d.authenticate()
|
|
d.tokenUpdate()
|
|
}
|
|
|
|
func (d *device) authenticate() {
|
|
payload := map[string]any{
|
|
"MessageType": "Authenticate",
|
|
"UDID": d.uuid,
|
|
"Model": d.model,
|
|
"DeviceName": "testdevice" + d.serial,
|
|
"Topic": "com.apple.mgmt.External." + d.uuid,
|
|
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
|
"SerialNumber": d.serial,
|
|
}
|
|
d.request("application/x-apple-aspen-mdm-checkin", payload)
|
|
}
|
|
|
|
func (d *device) tokenUpdate() {
|
|
payload := map[string]any{
|
|
"MessageType": "TokenUpdate",
|
|
"UDID": d.uuid,
|
|
"Topic": "com.apple.mgmt.External." + d.uuid,
|
|
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
|
"NotOnConsole": "false",
|
|
"PushMagic": "pushmagic" + d.serial,
|
|
"Token": []byte("token" + d.serial),
|
|
}
|
|
d.request("application/x-apple-aspen-mdm-checkin", payload)
|
|
}
|
|
|
|
func (d *device) checkout() {
|
|
payload := map[string]any{
|
|
"MessageType": "CheckOut",
|
|
"Topic": "com.apple.mgmt.External." + d.uuid,
|
|
"UDID": d.uuid,
|
|
"EnrollmentID": "testenrollmentid-" + d.uuid,
|
|
}
|
|
d.request("application/x-apple-aspen-mdm-checkin", payload)
|
|
}
|
|
|
|
func (d *device) request(reqType string, payload map[string]any) {
|
|
body, err := plist.Marshal(payload)
|
|
require.NoError(d.s.T(), err)
|
|
|
|
signedData, err := pkcs7.NewSignedData(body)
|
|
require.NoError(d.s.T(), err)
|
|
err = signedData.AddSigner(d.scepCert, d.scepKey, pkcs7.SignerInfoConfig{})
|
|
require.NoError(d.s.T(), err)
|
|
sig, err := signedData.Finish()
|
|
require.NoError(d.s.T(), err)
|
|
|
|
d.s.DoRawWithHeaders(
|
|
"POST",
|
|
"/mdm/apple/mdm",
|
|
body,
|
|
200,
|
|
map[string]string{
|
|
"Content-Type": reqType,
|
|
"Mdm-Signature": base64.StdEncoding.EncodeToString(sig),
|
|
},
|
|
)
|
|
}
|
|
|
|
func (d *device) scepEnroll() {
|
|
t := d.s.T()
|
|
ctx := context.Background()
|
|
logger := kitlog.NewJSONLogger(os.Stdout)
|
|
logger = level.NewFilter(logger, level.AllowDebug())
|
|
client, err := scepclient.New(d.s.server.URL+apple_mdm.SCEPPath, logger)
|
|
require.NoError(t, err)
|
|
|
|
resp, _, err := client.GetCACert(ctx, "")
|
|
require.NoError(t, err)
|
|
|
|
certs, err := x509.ParseCertificates(resp)
|
|
require.NoError(t, err)
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
|
|
csrTemplate := x509util.CertificateRequest{
|
|
CertificateRequest: x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: "fleet-test",
|
|
},
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
},
|
|
ChallengePassword: d.s.fleetCfg.MDMApple.SCEP.Challenge,
|
|
}
|
|
csrDerBytes, err := x509util.CreateCertificateRequest(rand.Reader, &csrTemplate, key)
|
|
require.NoError(t, err)
|
|
csr, err := x509.ParseCertificateRequest(csrDerBytes)
|
|
require.NoError(t, err)
|
|
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(time.Hour)
|
|
|
|
certTemplate := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
CommonName: "SCEP SIGNER",
|
|
Organization: csr.Subject.Organization,
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
certDerBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
|
require.NoError(t, err)
|
|
cert, err := x509.ParseCertificate(certDerBytes)
|
|
require.NoError(t, err)
|
|
|
|
tmpl := &scep.PKIMessage{
|
|
MessageType: scep.PKCSReq,
|
|
Recipients: certs,
|
|
SignerKey: key,
|
|
SignerCert: cert,
|
|
CSRReqMessage: &scep.CSRReqMessage{
|
|
ChallengePassword: d.s.fleetCfg.MDMApple.SCEP.Challenge,
|
|
},
|
|
}
|
|
|
|
msg, err := scep.NewCSRRequest(csr, tmpl, scep.WithLogger(logger))
|
|
require.NoError(t, err)
|
|
|
|
respBytes, err := client.PKIOperation(ctx, msg.Raw)
|
|
require.NoError(t, err)
|
|
|
|
respMsg, err := scep.ParsePKIMessage(respBytes, scep.WithLogger(logger), scep.WithCACerts(msg.Recipients))
|
|
require.NoError(t, err)
|
|
require.Equal(t, scep.SUCCESS, respMsg.PKIStatus)
|
|
|
|
err = respMsg.DecryptPKIEnvelope(cert, key)
|
|
require.NoError(t, err)
|
|
|
|
d.scepCert = respMsg.CertRepMessage.Certificate
|
|
d.scepKey = key
|
|
}
|
|
|
|
// numbers plus capital letters without I, L, O for readability
|
|
const serialLetters = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"
|
|
|
|
func randSerial() string {
|
|
b := make([]byte, 12)
|
|
for i := range b {
|
|
//nolint:gosec // not used for crypto, only to generate random serial for testing
|
|
b[i] = serialLetters[mathrand.Intn(len(serialLetters))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
var testBMToken = &nanodep_client.OAuth1Tokens{
|
|
ConsumerKey: "test_consumer",
|
|
ConsumerSecret: "test_secret",
|
|
AccessToken: "test_access_token",
|
|
AccessSecret: "test_access_secret",
|
|
AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
}
|