mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Issue #15057 (macOS flow) Manual QA: 1. Download a manual enrollment profile for a macOS device from the "My device" page (click on "Turn on MDM" banner). 2. Open the profile in a text editor and find the following plist entry: ```xml <key>PayloadIdentifier</key> <string>com.fleetdm.fleet.mdm.apple.mdm</string> ``` 3. Below that entry add a new entry as follows: ```xml <key>EndUserEmail</key> <string>user@example.com</string> ``` 4. Save the file, then double-click in Finder to activate the MDM enrollment notification. 5. Run orbit from source: ```sh go run github.com/fleetdm/fleet/v4/orbit/cmd/orbit \ --dev-mode \ --disable-updates \ --root-dir /tmp/orbit \ --fleet-url https://localhost:8080 \ --insecure \ --enroll-secret <YOUR ENROLL SECRET GOES HERE> \ -- --verbose ``` 6. Look for the following log entries in the terminal where you are running orbit: <img width="679" alt="Screenshot 2023-12-21 at 3 03 03 PM" src="https://github.com/fleetdm/fleet/assets/73313222/cefc77e3-e209-49b3-a03e-abff0f7f982b"> 7. In the UI, navigate to the host details page and check "Used by" in the "About" section: <img width="679" alt="Screenshot 2023-12-21 at 3 02 09 PM" src="https://github.com/fleetdm/fleet/assets/73313222/c58fff3e-cee7-4a94-a53b-f30f5b4bcfa0"> # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --------- Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
514 lines
14 KiB
Go
514 lines
14 KiB
Go
//go:build darwin
|
|
|
|
package profiles
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetFleetdConfig(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
cases := []struct {
|
|
cmdOut *string
|
|
cmdErr error
|
|
wantOut *fleet.MDMAppleFleetdConfig
|
|
wantErr error
|
|
}{
|
|
{nil, testErr, nil, testErr},
|
|
{ptr.String("invalid-xml"), nil, nil, io.EOF},
|
|
{&emptyOutput, nil, &fleet.MDMAppleFleetdConfig{}, nil},
|
|
{&withFleetdConfig, nil, &fleet.MDMAppleFleetdConfig{EnrollSecret: "ENROLL_SECRET", FleetURL: "https://test.example.com"}, nil},
|
|
}
|
|
|
|
origExecProfileCmd := execProfileCmd
|
|
t.Cleanup(func() { execProfileCmd = origExecProfileCmd })
|
|
for _, c := range cases {
|
|
execProfileCmd = func() (*bytes.Buffer, error) {
|
|
if c.cmdOut == nil {
|
|
return nil, c.cmdErr
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString(*c.cmdOut)
|
|
return &buf, nil
|
|
}
|
|
|
|
out, err := GetFleetdConfig()
|
|
require.ErrorIs(t, err, c.wantErr)
|
|
require.Equal(t, c.wantOut, out)
|
|
}
|
|
}
|
|
|
|
var (
|
|
emptyOutput = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict/>
|
|
</plist>`
|
|
|
|
withFleetdConfig = `
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>_computerlevel</key>
|
|
<array>
|
|
<dict>
|
|
<key>ProfileDescription</key>
|
|
<string>test descripiton</string>
|
|
<key>ProfileDisplayName</key>
|
|
<string>test name</string>
|
|
<key>ProfileIdentifier</key>
|
|
<string>com.fleetdm.fleetd.config</string>
|
|
<key>ProfileInstallDate</key>
|
|
<string>2023-02-27 18:55:07 +0000</string>
|
|
<key>ProfileItems</key>
|
|
<array>
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<dict>
|
|
<key>EnrollSecret</key>
|
|
<string>ENROLL_SECRET</string>
|
|
<key>FleetURL</key>
|
|
<string>https://test.example.com</string>
|
|
</dict>
|
|
<key>PayloadDescription</key>
|
|
<string>test description</string>
|
|
<key>PayloadDisplayName</key>
|
|
<string>test name</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.fleetdm.fleetd.config</string>
|
|
<key>PayloadType</key>
|
|
<string>com.fleetdm.fleetd</string>
|
|
<key>PayloadUUID</key>
|
|
<string>0C6AFB45-01B6-4E19-944A-123CD16381C7</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</array>
|
|
<key>ProfileRemovalDisallowed</key>
|
|
<string>true</string>
|
|
<key>ProfileType</key>
|
|
<string>Configuration</string>
|
|
<key>ProfileUUID</key>
|
|
<string>8D0F62E6-E24F-4B2F-AFA8-CAC1F07F4FDC</string>
|
|
<key>ProfileVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</array>
|
|
</dict>
|
|
</plist>`
|
|
|
|
withFleetdConfigAndEnrollment = `
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>_computerlevel</key>
|
|
<array>
|
|
<dict>
|
|
<key>ProfileDescription</key>
|
|
<string>test descripiton</string>
|
|
<key>ProfileDisplayName</key>
|
|
<string>test name</string>
|
|
<key>ProfileIdentifier</key>
|
|
<string>com.fleetdm.fleetd.config</string>
|
|
<key>ProfileInstallDate</key>
|
|
<string>2023-02-27 18:55:07 +0000</string>
|
|
<key>ProfileItems</key>
|
|
<array>
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<dict>
|
|
<key>EnrollSecret</key>
|
|
<string>ENROLL_SECRET</string>
|
|
<key>FleetURL</key>
|
|
<string>https://test.example.com</string>
|
|
</dict>
|
|
<key>PayloadDescription</key>
|
|
<string>test description</string>
|
|
<key>PayloadDisplayName</key>
|
|
<string>test name</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.fleetdm.fleetd.config</string>
|
|
<key>PayloadType</key>
|
|
<string>com.fleetdm.fleetd</string>
|
|
<key>PayloadUUID</key>
|
|
<string>0C6AFB45-01B6-4E19-944A-123CD16381C7</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</array>
|
|
<key>ProfileRemovalDisallowed</key>
|
|
<string>true</string>
|
|
<key>ProfileType</key>
|
|
<string>Configuration</string>
|
|
<key>ProfileUUID</key>
|
|
<string>8D0F62E6-E24F-4B2F-AFA8-CAC1F07F4FDC</string>
|
|
<key>ProfileVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
<dict>
|
|
<key>ProfileDisplayName</key>
|
|
<string>f1337 enrollment</string>
|
|
<key>ProfileIdentifier</key>
|
|
<string>com.fleetdm.fleet.mdm.apple</string>
|
|
<key>ProfileInstallDate</key>
|
|
<string>2023-02-27 18:55:07 +0000</string>
|
|
<key>ProfileItems</key>
|
|
<array>
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<dict/>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.fleetdm.fleet.mdm.apple.scep</string>
|
|
<key>PayloadType</key>
|
|
<string>com.apple.security.scep</string>
|
|
<key>PayloadUUID</key>
|
|
<string>BCA53F9D-5DD2-494D-98D3-0D0F20FF6BA1</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<dict>
|
|
<key>AccessRights</key>
|
|
<integer>8191</integer>
|
|
<key>CheckOutWhenRemoved</key>
|
|
<true/>
|
|
<key>EndUserEmail</key>
|
|
<string>user@example.com</string>
|
|
<key>ServerCapabilities</key>
|
|
<array>
|
|
<string>com.apple.mdm.per-user-connections</string>
|
|
<string>com.apple.mdm.bootstraptoken</string>
|
|
</array>
|
|
<key>ServerURL</key>
|
|
<string>https://test.example.com</string>
|
|
</dict>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.fleetdm.fleet.mdm.apple.mdm</string>
|
|
<key>PayloadType</key>
|
|
<string>com.apple.mdm</string>
|
|
<key>PayloadUUID</key>
|
|
<string>29713130-1602-4D27-90C9-B822A295E44E</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</array>
|
|
<key>ProfileOrganization</key>
|
|
<string>f1337</string>
|
|
<key>ProfileType</key>
|
|
<string>Configuration</string>
|
|
<key>ProfileUUID</key>
|
|
<string>5ACABE91-CE30-4C05-93E3-B235C152404E</string>
|
|
<key>ProfileVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</array>
|
|
</dict>
|
|
</plist>`
|
|
)
|
|
|
|
func TestCustomInstallerWorkflow(t *testing.T) {
|
|
origExecProfileCmd := execProfileCmd
|
|
t.Cleanup(func() { execProfileCmd = origExecProfileCmd })
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
mockOut string
|
|
wantEmail string
|
|
wantErr error
|
|
}{
|
|
{"happy path", withFleetdConfigAndEnrollment, "user@example.com", nil},
|
|
{"empty profiles", emptyOutput, "", ErrNotFound},
|
|
{"no enrollment payload", withFleetdConfig, "", ErrNotFound},
|
|
{"wrong payload identifier", strings.Replace(withFleetdConfigAndEnrollment, mobileconfig.FleetEnrollmentPayloadIdentifier, "wrong-identifier", 1), "", ErrNotFound},
|
|
{"no end user email key", strings.Replace(withFleetdConfigAndEnrollment, "EndUserEmail", "WrongKey", 1), "", ErrNotFound},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
execProfileCmd = func() (*bytes.Buffer, error) {
|
|
var buf bytes.Buffer
|
|
buf.WriteString(c.mockOut)
|
|
return &buf, nil
|
|
}
|
|
|
|
gotContent, err := GetCustomEnrollmentProfileEndUserEmail()
|
|
if c.wantErr != nil {
|
|
require.ErrorIs(t, err, c.wantErr)
|
|
require.Empty(t, gotContent)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, "user@example.com", gotContent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsEnrolledInMDM(t *testing.T) {
|
|
cases := []struct {
|
|
cmdOut *string
|
|
cmdErr error
|
|
wantEnrolled bool
|
|
wantURL string
|
|
wantErr bool
|
|
}{
|
|
{nil, errors.New("test error"), false, "", true},
|
|
{ptr.String(""), nil, false, "", false},
|
|
{ptr.String(`
|
|
Enrolled via DEP: No
|
|
MDM enrollment: No
|
|
`), nil, false, "", false},
|
|
{
|
|
ptr.String(`
|
|
Enrolled via DEP: Yes
|
|
MDM enrollment: Yes
|
|
MDM server: https://test.example.com
|
|
`),
|
|
nil,
|
|
true,
|
|
"https://test.example.com",
|
|
false,
|
|
},
|
|
{
|
|
ptr.String(`
|
|
Enrolled via DEP: Yes
|
|
MDM enrollment: Yes
|
|
MDM server / https://test.example.com
|
|
`),
|
|
nil,
|
|
true,
|
|
"//test.example.com",
|
|
false,
|
|
},
|
|
{
|
|
ptr.String(`
|
|
Enrolled via DEP: Yes
|
|
MDM enrollment: Yes
|
|
MDM server: https://valid.com/mdm/apple/mdm
|
|
`),
|
|
nil,
|
|
true,
|
|
"https://valid.com/mdm/apple/mdm",
|
|
false,
|
|
},
|
|
}
|
|
|
|
origCmd := getMDMInfoFromProfilesCmd
|
|
t.Cleanup(func() { getMDMInfoFromProfilesCmd = origCmd })
|
|
for _, c := range cases {
|
|
getMDMInfoFromProfilesCmd = func() ([]byte, error) {
|
|
if c.cmdOut == nil {
|
|
return nil, c.cmdErr
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString(*c.cmdOut)
|
|
return []byte(*c.cmdOut), nil
|
|
}
|
|
|
|
enrolled, url, err := IsEnrolledInMDM()
|
|
if c.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, c.wantEnrolled, enrolled)
|
|
require.Equal(t, c.wantURL, url)
|
|
}
|
|
}
|
|
|
|
func TestCheckAssignedEnrollmentProfile(t *testing.T) {
|
|
fleetURL := "https://valid.com"
|
|
cases := []struct {
|
|
name string
|
|
cmdOut *string
|
|
cmdErr error
|
|
wantOut bool
|
|
wantErr error
|
|
}{
|
|
{
|
|
"command error",
|
|
nil,
|
|
errors.New("some command error"),
|
|
false,
|
|
errors.New("some command error"),
|
|
},
|
|
{
|
|
"empty output",
|
|
ptr.String(""),
|
|
nil,
|
|
false,
|
|
errors.New("parsing profiles output: expected at least 2 lines but got 1"),
|
|
},
|
|
{
|
|
"null profile",
|
|
ptr.String(`Device Enrollment configuration:
|
|
(null)
|
|
`),
|
|
nil,
|
|
false,
|
|
errors.New("parsing profiles output: received null device enrollment configuration"),
|
|
},
|
|
{
|
|
"mismatch profile",
|
|
ptr.String(`Device Enrollment configuration:
|
|
{
|
|
AllowPairing = 1;
|
|
AutoAdvanceSetup = 0;
|
|
AwaitDeviceConfigured = 0;
|
|
ConfigurationURL = "https://test.example.com/mdm/apple/enroll?token=1234";
|
|
ConfigurationWebURL = "https://test.example.com/mdm/apple/enroll?token=1234";
|
|
...
|
|
}
|
|
`),
|
|
nil,
|
|
false,
|
|
errors.New(`configuration web url: expected 'valid.com' but found 'test.example.com'`),
|
|
},
|
|
{
|
|
"match profile",
|
|
ptr.String(`Device Enrollment configuration:
|
|
{
|
|
AllowPairing = 1;
|
|
AutoAdvanceSetup = 0;
|
|
AwaitDeviceConfigured = 0;
|
|
ConfigurationURL = "https://test.example.com/mdm/apple/enroll?token=1234";
|
|
ConfigurationWebURL = "https://valid.com?token=1234";
|
|
...
|
|
}
|
|
`),
|
|
nil,
|
|
false,
|
|
nil,
|
|
},
|
|
{
|
|
"mixed case match",
|
|
ptr.String(`Device Enrollment configuration:
|
|
{
|
|
AllowPairing = 1;
|
|
AutoAdvanceSetup = 0;
|
|
AwaitDeviceConfigured = 0;
|
|
ConfigurationURL = "https://test.ExaMplE.com/mdm/apple/enroll?token=1234";
|
|
ConfigurationWebURL = "https://vaLiD.com?tOken=1234";
|
|
...
|
|
}
|
|
`),
|
|
nil,
|
|
false,
|
|
nil,
|
|
},
|
|
}
|
|
|
|
origCmd := showEnrollmentProfileCmd
|
|
t.Cleanup(func() { showEnrollmentProfileCmd = origCmd })
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
showEnrollmentProfileCmd = func() ([]byte, error) {
|
|
if c.cmdOut == nil {
|
|
return nil, c.cmdErr
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteString(*c.cmdOut)
|
|
return []byte(*c.cmdOut), nil
|
|
}
|
|
|
|
err := CheckAssignedEnrollmentProfile(fleetURL)
|
|
if c.wantErr != nil {
|
|
require.ErrorContains(t, err, c.wantErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetProfilePayloadContent(t *testing.T) {
|
|
origExecProfileCmd := execProfileCmd
|
|
t.Cleanup(func() { execProfileCmd = origExecProfileCmd })
|
|
|
|
execProfileCmd = func() (*bytes.Buffer, error) {
|
|
var buf bytes.Buffer
|
|
buf.WriteString(withFleetdConfigAndEnrollment)
|
|
return &buf, nil
|
|
}
|
|
|
|
// mismatched int type is not acceptable
|
|
_, err := getProfilePayloadContent[int]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.ErrorContains(t, err, "plist: cannot unmarshal dict into Go value of type int")
|
|
|
|
// mismatched string type is not acceptable
|
|
_, err = getProfilePayloadContent[string]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.ErrorContains(t, err, "plist: cannot unmarshal dict into Go value of type string")
|
|
|
|
// mismatched bool type is not acceptable
|
|
_, err = getProfilePayloadContent[bool]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.ErrorContains(t, err, "plist: cannot unmarshal dict into Go value of type bool")
|
|
|
|
// mismatched slice type is not acceptable
|
|
_, err = getProfilePayloadContent[[]string]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.ErrorContains(t, err, "plist: cannot unmarshal dict into Go value of type []string")
|
|
|
|
// mismatched []byte type is not acceptable
|
|
_, err = getProfilePayloadContent[[]byte]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.ErrorContains(t, err, "plist: cannot unmarshal dict into Go value of type []uint8")
|
|
|
|
// mismatched struct type is acceptable, but result is empty
|
|
type wrongStruct struct {
|
|
foo string
|
|
bar int
|
|
}
|
|
ws, err := getProfilePayloadContent[wrongStruct]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ws)
|
|
require.Empty(t, ws.bar)
|
|
require.Empty(t, ws.foo)
|
|
|
|
// struct type is acceptable and returns the expected value type corresponds to the payload identifier
|
|
c, err := getProfilePayloadContent[fleet.MDMAppleFleetdConfig]("com.fleetdm.fleetd.config")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, c)
|
|
require.Equal(t, *c, fleet.MDMAppleFleetdConfig{EnrollSecret: "ENROLL_SECRET", FleetURL: "https://test.example.com"})
|
|
|
|
// struct type is acceptable and returns the expected value type corresponds to the payload identifier
|
|
e, err := getProfilePayloadContent[fleet.MDMCustomEnrollmentProfileItem]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, e)
|
|
require.Equal(t, *e, fleet.MDMCustomEnrollmentProfileItem{EndUserEmail: "user@example.com"})
|
|
|
|
// map type is acceptable
|
|
m, err := getProfilePayloadContent[map[string]any]("com.fleetdm.fleet.mdm.apple.mdm")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, m)
|
|
gotMap := *m
|
|
v, ok := gotMap["EndUserEmail"]
|
|
require.True(t, ok)
|
|
require.Equal(t, "user@example.com", v)
|
|
_, ok = gotMap["EnrollSecret"]
|
|
require.False(t, ok)
|
|
_, ok = gotMap["FleetURL"]
|
|
require.False(t, ok)
|
|
|
|
// map type is acceptable
|
|
m2, err := getProfilePayloadContent[map[string]any]("com.fleetdm.fleetd.config")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, m)
|
|
gotMap = *m2
|
|
v, ok = gotMap["EnrollSecret"]
|
|
require.True(t, ok)
|
|
require.Equal(t, "ENROLL_SECRET", v)
|
|
v, ok = gotMap["FleetURL"]
|
|
require.True(t, ok)
|
|
require.Equal(t, "https://test.example.com", v)
|
|
_, ok = gotMap["EndUserEmail"]
|
|
require.False(t, ok)
|
|
}
|