Add support for the ApplicationConfiguration setting (#521)

The ApplicationConfiguration setting is implemented by storing the values for Configuration as a plist, and then marshaling it into the settings command as a dictionary.
This commit is contained in:
Victor Vrantchan
2018-10-13 13:54:43 -04:00
committed by GitHub
parent 3707d489a6
commit 8af0ef7bd1
9 changed files with 674 additions and 309 deletions

View File

@@ -6,6 +6,7 @@
* Add API endpoint to retrieve APNS certificate (#503)
* Remove deprecated `-apns` flags from server startup (#528)
* Move API calls to list endpoints from HTTP GET to HTTP POST (#522, #523, #524, #525, #526)
* Add support for the ApplicationConfiguration Setting (#521)
## [v1.4.0](https://github.com/micromdm/micromdm/compare/v1.3.1...v1.4.0) September 6 2018

View File

@@ -218,6 +218,8 @@ type Setting struct {
MDMOptions map[string]interface{} `plist:",omitempty" json:"mdm_options,omitempty"`
PasscodeLockGracePeriod *int `plist:",omitempty" json:"passcode_lock_grace_period,omitempty"`
MaximumResidentUsers *int `plist:",omitempty" json:"maximum_resident_users,omitempty"`
Configuration map[string]interface{} `plist:",omitempty" json:"-"`
ConfigurationData []byte `plist:"-" json:"configuration"` // used to build the dictionary
}
type ManagedApplicationConfiguration struct {

File diff suppressed because it is too large Load Diff

View File

@@ -259,6 +259,7 @@ message Setting {
MaximumResidentUsersSetting maximum_resident_users = 12;
DiagnosticSubmissionSetting diagnostic_submission = 13;
AppAnalyticsSetting app_analytics = 14;
ApplicationConfigurationSetting application_configuration = 15;
}
message VoiceRoamingSetting {
@@ -287,6 +288,11 @@ message ApplicationAttributesSetting {
ApplicationAttributes application_attributes = 2;
}
message ApplicationConfigurationSetting {
string identifier = 1;
bytes configuration_dictionary_data = 2; // A serialized plist of the dictionary.
}
message ApplicationAttributes {
string vpn_uuid = 1;
}

View File

@@ -1,6 +1,11 @@
package mdm
import "fmt"
import (
"fmt"
"github.com/groob/plist"
"github.com/pkg/errors"
)
func (c *Command) MarshalPlist() (interface{}, error) {
switch c.RequestType {
@@ -221,6 +226,17 @@ func (c *Command) MarshalPlist() (interface{}, error) {
RemoveMedia: c.RemoveMedia,
}, nil
case "Settings":
// convert all the data plists into the dictionary inside settings before serialization.
for i, set := range c.Settings.Settings {
if len(set.ConfigurationData) > 0 {
var configuration map[string]interface{}
if err := plist.Unmarshal(set.ConfigurationData, &configuration); err != nil {
return nil, errors.Wrap(err, "turning the configuration data plist into a dictionary")
}
set.Configuration = configuration
c.Settings.Settings[i] = set
}
}
return &struct {
RequestType string
*Settings

View File

@@ -346,6 +346,11 @@ func commandToProto(cmd *Command) (*mdmproto.Command, error) {
func settingToProto(s Setting) *mdmproto.Setting {
pbs := mdmproto.Setting{Item: s.Item}
switch s.Item {
case "ApplicationConfiguration":
pbs.ApplicationConfiguration = &mdmproto.ApplicationConfigurationSetting{
Identifier: emptyStringIfNil(s.Identifier),
ConfigurationDictionaryData: s.ConfigurationData,
}
case "VoiceRoaming":
pbs.VoiceRoaming = &mdmproto.VoiceRoamingSetting{
Enabled: falseIfNil(s.Enabled),

View File

@@ -135,49 +135,106 @@ func testCommandUnmarshal(
}
func TestEndToEnd(t *testing.T) {
// given an request that came over http as JSON
requestBytes := []byte(`{"udid": "BC5E2DA4-7FB6-5E70-9928-4981680DAFBF", "payload":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+UGF5bG9hZENvbnRlbnQ8L2tleT4KCTxhcnJheT4KCQk8ZGljdD4KCQkJPGtleT5QYXlsb2FkQ29udGVudDwva2V5PgoJCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLmFzc2lzdGFudC5zdXBwb3J0PC9rZXk+CgkJCQk8ZGljdD4KCQkJCQk8a2V5PkZvcmNlZDwva2V5PgoJCQkJCTxhcnJheT4KCQkJCQkJPGRpY3Q+CgkJCQkJCQk8a2V5Pm1jeF9wcmVmZXJlbmNlX3NldHRpbmdzPC9rZXk+CgkJCQkJCQk8ZGljdD4KCQkJCQkJCQk8a2V5PkFzc2lzdGFudCBFbmFibGVkPC9rZXk+CgkJCQkJCQkJPGZhbHNlLz4KCQkJCQkJCTwvZGljdD4KCQkJCQkJPC9kaWN0PgoJCQkJCTwvYXJyYXk+CgkJCQk8L2RpY3Q+CgkJCTwvZGljdD4KCQkJPGtleT5QYXlsb2FkRW5hYmxlZDwva2V5PgoJCQk8dHJ1ZS8+CgkJCTxrZXk+UGF5bG9hZElkZW50aWZpZXI8L2tleT4KCQkJPHN0cmluZz5NQ1hUb1Byb2ZpbGUuOWM3MzgwZDItNWJmZS00ZTYwLWJhZDMtMjVhZDg2ZDYxNTBkLmFsYWNhcnRlLmN1c3RvbXNldHRpbmdzLmZiOTU4ZDk2LWE0MzEtNDUzNi04NGQwLTFiZTQ4MjM4NWZiMjwvc3RyaW5nPgoJCQk8a2V5PlBheWxvYWRUeXBlPC9rZXk+CgkJCTxzdHJpbmc+Y29tLmFwcGxlLk1hbmFnZWRDbGllbnQucHJlZmVyZW5jZXM8L3N0cmluZz4KCQkJPGtleT5QYXlsb2FkVVVJRDwva2V5PgoJCQk8c3RyaW5nPmZiOTU4ZDk2LWE0MzEtNDUzNi04NGQwLTFiZTQ4MjM4NWZiMjwvc3RyaW5nPgoJCQk8a2V5PlBheWxvYWRWZXJzaW9uPC9rZXk+CgkJCTxpbnRlZ2VyPjE8L2ludGVnZXI+CgkJPC9kaWN0PgoJPC9hcnJheT4KCTxrZXk+UGF5bG9hZERlc2NyaXB0aW9uPC9rZXk+Cgk8c3RyaW5nPlN0b3BzIFNpcmkgZnJvbSBiZWluZyBlbmFibGVkLjwvc3RyaW5nPgoJPGtleT5QYXlsb2FkRGlzcGxheU5hbWU8L2tleT4KCTxzdHJpbmc+RGlzYWJsZSBTaXJpPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRJZGVudGlmaWVyPC9rZXk+Cgk8c3RyaW5nPkRpc2FibGVTaXJpPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRPcmdhbml6YXRpb248L2tleT4KCTxzdHJpbmc+PC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRSZW1vdmFsRGlzYWxsb3dlZDwva2V5PgoJPHRydWUvPgoJPGtleT5QYXlsb2FkU2NvcGU8L2tleT4KCTxzdHJpbmc+U3lzdGVtPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRUeXBlPC9rZXk+Cgk8c3RyaW5nPkNvbmZpZ3VyYXRpb248L3N0cmluZz4KCTxrZXk+UGF5bG9hZFVVSUQ8L2tleT4KCTxzdHJpbmc+OWM3MzgwZDItNWJmZS00ZTYwLWJhZDMtMjVhZDg2ZDYxNTBkPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRWZXJzaW9uPC9rZXk+Cgk8aW50ZWdlcj4xPC9pbnRlZ2VyPgo8L2RpY3Q+CjwvcGxpc3Q+Cg==", "request_type": "InstallProfile"}`)
var tests = []struct {
name string
requestBytes []byte
testFn func(t *testing.T, parts endToEndParts)
}{
{
name: "Settings_ApplicationConfiguration",
requestBytes: []byte(
`{"udid":"BC5E2DA4-7FB6-5E70-9928-4981680DAFBF","request_type":"Settings","settings":[{"item":"ApplicationConfiguration","identifier":"anidentifier","configuration":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgogIDxkaWN0PgogICAgPGtleT5iYXo8L2tleT4KICAgIDxzdHJpbmc+cXV4PC9zdHJpbmc+CiAgICA8a2V5PmNvdW50PC9rZXk+CiAgICA8aW50ZWdlcj4xPC9pbnRlZ2VyPgogICAgPGtleT5mb288L2tleT4KICAgIDxzdHJpbmc+YmFyPC9zdHJpbmc+CiAgPC9kaWN0Pgo8L3BsaXN0Pgo="}]}`,
),
testFn: func(t *testing.T, parts endToEndParts) {
if len(parts.req.Command.Settings.Settings) == 0 {
t.Error("expected settings command to include at least one setting")
}
// unmarshal the request into a go structure
var req CommandRequest
if err := json.Unmarshal(requestBytes, &req); err != nil {
t.Fatal(err)
}
if len(req.Command.InstallProfile.Payload) == 0 {
t.Fatal("InstallProfile payload is empty after json unmarshal")
if len(parts.fromProto.Command.Settings.Settings) == 0 {
t.Error("expected settings command from proto to include at least one setting")
}
// unmarshal plist and check that the settings in the configuration dictionary are there
var cmd struct {
Command struct{ Settings []map[string]interface{} }
}
if err := plist.Unmarshal(parts.plistData, &cmd); err != nil {
t.Fatal(err)
}
setting := cmd.Command.Settings[0]["Configuration"].(map[string]interface{})
if have, want := setting["foo"], "bar"; have != want {
t.Errorf("have key %s, want key %s", have, want)
}
},
},
{
name: "InstallProfile",
requestBytes: []byte(
`{"udid": "BC5E2DA4-7FB6-5E70-9928-4981680DAFBF", "payload":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+UGF5bG9hZENvbnRlbnQ8L2tleT4KCTxhcnJheT4KCQk8ZGljdD4KCQkJPGtleT5QYXlsb2FkQ29udGVudDwva2V5PgoJCQk8ZGljdD4KCQkJCTxrZXk+Y29tLmFwcGxlLmFzc2lzdGFudC5zdXBwb3J0PC9rZXk+CgkJCQk8ZGljdD4KCQkJCQk8a2V5PkZvcmNlZDwva2V5PgoJCQkJCTxhcnJheT4KCQkJCQkJPGRpY3Q+CgkJCQkJCQk8a2V5Pm1jeF9wcmVmZXJlbmNlX3NldHRpbmdzPC9rZXk+CgkJCQkJCQk8ZGljdD4KCQkJCQkJCQk8a2V5PkFzc2lzdGFudCBFbmFibGVkPC9rZXk+CgkJCQkJCQkJPGZhbHNlLz4KCQkJCQkJCTwvZGljdD4KCQkJCQkJPC9kaWN0PgoJCQkJCTwvYXJyYXk+CgkJCQk8L2RpY3Q+CgkJCTwvZGljdD4KCQkJPGtleT5QYXlsb2FkRW5hYmxlZDwva2V5PgoJCQk8dHJ1ZS8+CgkJCTxrZXk+UGF5bG9hZElkZW50aWZpZXI8L2tleT4KCQkJPHN0cmluZz5NQ1hUb1Byb2ZpbGUuOWM3MzgwZDItNWJmZS00ZTYwLWJhZDMtMjVhZDg2ZDYxNTBkLmFsYWNhcnRlLmN1c3RvbXNldHRpbmdzLmZiOTU4ZDk2LWE0MzEtNDUzNi04NGQwLTFiZTQ4MjM4NWZiMjwvc3RyaW5nPgoJCQk8a2V5PlBheWxvYWRUeXBlPC9rZXk+CgkJCTxzdHJpbmc+Y29tLmFwcGxlLk1hbmFnZWRDbGllbnQucHJlZmVyZW5jZXM8L3N0cmluZz4KCQkJPGtleT5QYXlsb2FkVVVJRDwva2V5PgoJCQk8c3RyaW5nPmZiOTU4ZDk2LWE0MzEtNDUzNi04NGQwLTFiZTQ4MjM4NWZiMjwvc3RyaW5nPgoJCQk8a2V5PlBheWxvYWRWZXJzaW9uPC9rZXk+CgkJCTxpbnRlZ2VyPjE8L2ludGVnZXI+CgkJPC9kaWN0PgoJPC9hcnJheT4KCTxrZXk+UGF5bG9hZERlc2NyaXB0aW9uPC9rZXk+Cgk8c3RyaW5nPlN0b3BzIFNpcmkgZnJvbSBiZWluZyBlbmFibGVkLjwvc3RyaW5nPgoJPGtleT5QYXlsb2FkRGlzcGxheU5hbWU8L2tleT4KCTxzdHJpbmc+RGlzYWJsZSBTaXJpPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRJZGVudGlmaWVyPC9rZXk+Cgk8c3RyaW5nPkRpc2FibGVTaXJpPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRPcmdhbml6YXRpb248L2tleT4KCTxzdHJpbmc+PC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRSZW1vdmFsRGlzYWxsb3dlZDwva2V5PgoJPHRydWUvPgoJPGtleT5QYXlsb2FkU2NvcGU8L2tleT4KCTxzdHJpbmc+U3lzdGVtPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRUeXBlPC9rZXk+Cgk8c3RyaW5nPkNvbmZpZ3VyYXRpb248L3N0cmluZz4KCTxrZXk+UGF5bG9hZFVVSUQ8L2tleT4KCTxzdHJpbmc+OWM3MzgwZDItNWJmZS00ZTYwLWJhZDMtMjVhZDg2ZDYxNTBkPC9zdHJpbmc+Cgk8a2V5PlBheWxvYWRWZXJzaW9uPC9rZXk+Cgk8aW50ZWdlcj4xPC9pbnRlZ2VyPgo8L2RpY3Q+CjwvcGxpc3Q+Cg==", "request_type": "InstallProfile"}`,
),
testFn: func(t *testing.T, parts endToEndParts) {
if len(parts.req.Command.InstallProfile.Payload) == 0 {
t.Error("InstallProfile payload is empty after json unmarshal")
}
if len(parts.fromProto.Command.InstallProfile.Payload) == 0 {
t.Error("unmarshaled proto payload is missing payload")
}
if !bytes.Contains(parts.plistData, []byte(`PD94bWwgdm`)) {
t.Error("marshaled plist does not contain the required payload")
}
},
},
}
// create a payload from the request
payload, err := NewCommandPayload(&req)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(payload.Command.InstallProfile.Payload, req.Command.InstallProfile.Payload) {
t.Fatal("mdm payload and request do not have the same payload data")
}
// marshal to proto (for storage)
data, err := MarshalCommandPayload(payload)
if err != nil {
t.Fatal(err)
}
// unmarshal the proto back into go (from storage)
var newPayload CommandPayload
err = UnmarshalCommandPayload(data, &newPayload)
if err != nil {
t.Fatal(err)
}
if len(newPayload.Command.InstallProfile.Payload) == 0 {
t.Fatal("unmarshaled proto payload is missing payload")
}
// marshal it into a plist to send to the device
pd, err := plist.MarshalIndent(newPayload, " ")
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(pd, []byte(`PD94bWwgdm`)) {
t.Fatal("marshaled plist does not contain the required payload")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parts := endToEnd(t, tt.requestBytes)
tt.testFn(t, parts)
})
}
}
type endToEndParts struct {
requestBytes []byte // some json, our API request
req CommandRequest // after unmarshal
payload *CommandPayload // new payload
protoData []byte // stored as
fromProto CommandPayload // back from proto
plistData []byte // final representation
}
func endToEnd(t *testing.T, requestBytes []byte) endToEndParts {
t.Helper()
var (
err error
parts = endToEndParts{requestBytes: requestBytes}
)
if err = json.Unmarshal(parts.requestBytes, &parts.req); err != nil {
t.Fatal(err)
}
if parts.payload, err = NewCommandPayload(&parts.req); err != nil {
t.Fatal(err)
}
if parts.protoData, err = MarshalCommandPayload(parts.payload); err != nil {
t.Fatal(err)
}
if err := UnmarshalCommandPayload(parts.protoData, &parts.fromProto); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(*parts.payload, parts.fromProto) {
t.Errorf("command from json request does not match command from proto")
}
if parts.plistData, err = plist.MarshalIndent(parts.fromProto, " "); err != nil {
t.Fatal(err)
}
return parts
}

View File

@@ -302,6 +302,10 @@ func protoToSetting(s *mdmproto.Setting) Setting {
Item: s.GetItem(),
}
switch s.Item {
case "ApplicationConfiguration":
pbs := s.GetApplicationConfiguration()
setting.Identifier = nilIfEmptyString(pbs.GetIdentifier())
setting.ConfigurationData = pbs.GetConfigurationDictionaryData()
case "VoiceRoaming":
pbs := s.GetVoiceRoaming()
setting.Enabled = nilIfFalse(pbs.GetEnabled())

View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Documentation:
# The configuration values must be provided to $3 must be a property list with the key/values for the configuration dictionary.
# Example:
#
# <?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>baz</key>
# <string>qux</string>
# <key>count</key>
# <integer>1</integer>
# <key>foo</key>
# <string>bar</string>
# </dict>
# </plist>
source $MICROMDM_ENV_PATH
endpoint="v1/commands"
jq -n \
--arg request_type "Settings" \
--arg udid "$1" \
--arg identifier "$2" \
--arg payload "$(cat $3|openssl base64 -A)" \
'.udid = $udid
|.request_type = $request_type
|.settings[0].item = "ApplicationConfiguration"
|.settings[0].identifier = $identifier
|.settings[0].configuration = $payload
'|\
curl $CURL_OPTS -u "micromdm:$API_TOKEN" "$SERVER_URL/$endpoint" -d@-