mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
read orbit profile configuration values using osascript in macOS (#12086)
The current approach to read the enroll secret and fleet url from a configuration profile is not ideal because: 1. (important) We're looking for a profile with a `ProfileIdentifier` equal to `com.fleetdm.fleetd.config`. This is not ideal because `ProfileIdentifier` is often modified by MDM vendors to ensure that's unique across all profiles in the system. 2. (nit) To look for the relevant profile, we were running `profiles list -o stdout-xml`, which can output a large amount of data that we need to parse and loop through to find the right profile. I have also considered: 1. Reading the value from a file that gets created at `/Library/Managed Preferences/com.fleetdm.fleetd.config.plist`, but I couldn't find any official sources on the reliablity of this, and after consulting internally and in the macAdmins slack I decided to not rely on it. 2. Keep on reading from the output of `profiles` but be smarter parsing the output (we should still be able to find the right profile) At the end, I decided to use osascript to read the value directly from the system.
This commit is contained in:
parent
2e13b9331e
commit
6e3248237c
3 changed files with 55 additions and 109 deletions
1
orbit/changes/11692-profile-config-read
Normal file
1
orbit/changes/11692-profile-config-read
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Improve the logic to read enroll secrets from macOS configuration profiles to be compatible with different MDM providers.
|
||||
|
|
@ -4,66 +4,49 @@ package profiles
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/groob/plist"
|
||||
)
|
||||
|
||||
type profileItem struct {
|
||||
PayloadContent fleet.MDMAppleFleetdConfig
|
||||
PayloadType string
|
||||
}
|
||||
|
||||
type profilePayload struct {
|
||||
ProfileIdentifier string
|
||||
ProfileItems []profileItem
|
||||
}
|
||||
|
||||
type profilesOutput struct {
|
||||
ComputerLevel []profilePayload `plist:"_computerlevel"`
|
||||
}
|
||||
|
||||
// GetFleetdConfig searches and parses a device level configuration profile
|
||||
// with Fleet's payload identifier.
|
||||
// GetFleetdConfig reads a system level setting set with Fleet's payload identifier.
|
||||
func GetFleetdConfig() (*fleet.MDMAppleFleetdConfig, error) {
|
||||
p, err := getProfile(mobileconfig.FleetdConfigPayloadIdentifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readFleetdConfigAppleScript := fmt.Sprintf(`
|
||||
const config = $.NSUserDefaults.alloc.initWithSuiteName("%s");
|
||||
const enrollSecret = config.objectForKey("EnrollSecret");
|
||||
const fleetURL = config.objectForKey("FleetURL");
|
||||
JSON.stringify({
|
||||
EnrollSecret: ObjC.deepUnwrap(enrollSecret),
|
||||
FleetURL: ObjC.deepUnwrap(fleetURL),
|
||||
});
|
||||
`, mobileconfig.FleetdConfigPayloadIdentifier)
|
||||
|
||||
return &p.ProfileItems[0].PayloadContent, nil
|
||||
}
|
||||
|
||||
func getProfile(identifier string) (*profilePayload, error) {
|
||||
outBuf, err := execProfileCmd()
|
||||
outBuf, err := execScript(readFleetdConfigAppleScript)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get profile: %w", err)
|
||||
}
|
||||
|
||||
var profiles profilesOutput
|
||||
if err := plist.Unmarshal(outBuf.Bytes(), &profiles); err != nil {
|
||||
return nil, fmt.Errorf("get profile: %w", err)
|
||||
var cfg fleet.MDMAppleFleetdConfig
|
||||
if err = json.Unmarshal(outBuf.Bytes(), &cfg); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling configuration: %w", err)
|
||||
}
|
||||
|
||||
for _, profile := range profiles.ComputerLevel {
|
||||
if profile.ProfileIdentifier == identifier {
|
||||
return &profile, nil
|
||||
}
|
||||
if cfg.EnrollSecret == "" || cfg.FleetURL == "" {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
return nil, ErrNotFound
|
||||
return &cfg, err
|
||||
}
|
||||
|
||||
// execProfileCmd is declared as a variable so it can be overwritten by tests.
|
||||
var execProfileCmd = func() (*bytes.Buffer, error) {
|
||||
// execScript is declared as a variable so it can be overwritten by tests.
|
||||
var execScript = func(script string) (*bytes.Buffer, error) {
|
||||
var outBuf bytes.Buffer
|
||||
cmd := exec.Command("/usr/bin/profiles", "list", "-o", "stdout-xml")
|
||||
cmd := exec.Command("osascript", "-l", "JavaScript", "-e", script)
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &outBuf
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package profiles
|
|||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -19,18 +18,38 @@ func TestGetFleetdConfig(t *testing.T) {
|
|||
cmdOut *string
|
||||
cmdErr error
|
||||
wantOut *fleet.MDMAppleFleetdConfig
|
||||
wantErr error
|
||||
wantErr string
|
||||
}{
|
||||
{nil, testErr, nil, testErr},
|
||||
{ptr.String("invalid-xml"), nil, nil, io.EOF},
|
||||
{&emptyOutput, nil, nil, ErrNotFound},
|
||||
{&withFleetdConfig, nil, &fleet.MDMAppleFleetdConfig{EnrollSecret: "ENROLL_SECRET", FleetURL: "https://test.example.com"}, nil},
|
||||
{nil, testErr, nil, testErr.Error()},
|
||||
{ptr.String("invalid-json"), nil, nil, "unmarshaling configuration"},
|
||||
{ptr.String("{}"), nil, nil, ErrNotFound.Error()},
|
||||
{
|
||||
ptr.String(`{"EnrollSecret": "ENROLL_SECRET", "FleetURL": "https://test.example.com"}`),
|
||||
nil,
|
||||
&fleet.MDMAppleFleetdConfig{
|
||||
EnrollSecret: "ENROLL_SECRET",
|
||||
FleetURL: "https://test.example.com",
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
ptr.String(`{"EnrollSecret": "ENROLL_SECRET", "FleetURL": ""}`),
|
||||
nil,
|
||||
nil,
|
||||
ErrNotFound.Error(),
|
||||
},
|
||||
{
|
||||
ptr.String(`{"EnrollSecret": "", "FleetURL": "https://test.example.com"}`),
|
||||
nil,
|
||||
nil,
|
||||
ErrNotFound.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
origExecProfileCmd := execProfileCmd
|
||||
t.Cleanup(func() { execProfileCmd = origExecProfileCmd })
|
||||
origExecScript := execScript
|
||||
t.Cleanup(func() { execScript = origExecScript })
|
||||
for _, c := range cases {
|
||||
execProfileCmd = func() (*bytes.Buffer, error) {
|
||||
execScript = func(script string) (*bytes.Buffer, error) {
|
||||
if c.cmdOut == nil {
|
||||
return nil, c.cmdErr
|
||||
}
|
||||
|
|
@ -41,69 +60,12 @@ func TestGetFleetdConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
out, err := GetFleetdConfig()
|
||||
require.ErrorIs(t, err, c.wantErr)
|
||||
if c.wantErr != "" {
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
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>`
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue