fleet/orbit/pkg/profiles/profiles_darwin_test.go
Sarah Gillespie 583d31b721
Update fleetd for macOS hosts to look for custom end user email field in Fleet MDM enrollment profile (#15761)
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>
2024-01-02 17:45:11 -03:00

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)
}