mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Add new data types and table for Apple MDM config profiles (#9758)
This commit is contained in:
parent
7cd581866a
commit
aca2449566
2
go.mod
2
go.mod
@ -49,6 +49,7 @@ require (
|
||||
github.com/groob/plist v0.0.0-20220217120414-63fa881b19a5
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95
|
||||
github.com/hillu/go-ntdll v0.0.0-20220801201350-0d23f057ef1f
|
||||
github.com/igm/sockjs-go/v3 v3.0.0
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5
|
||||
@ -225,7 +226,6 @@ require (
|
||||
github.com/hashicorp/go-version v1.2.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hillu/go-ntdll v0.0.0-20220801201350-0d23f057ef1f // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/iancoleman/orderedmap v0.2.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
|
@ -0,0 +1,37 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20230206163608, Down_20230206163608)
|
||||
}
|
||||
|
||||
func Up_20230206163608(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE mdm_apple_configuration_profiles (
|
||||
profile_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
team_id INT(10) UNSIGNED NOT NULL DEFAULT 0,
|
||||
-- team_id is zero for configuration profiles that are not associated with any team
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
mobileconfig BLOB NOT NULL,
|
||||
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (profile_id),
|
||||
UNIQUE KEY idx_mdm_apple_config_prof_team_identifier (team_id, identifier),
|
||||
UNIQUE KEY idx_mdm_apple_config_prof_team_name (team_id, name)
|
||||
);`)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "create table")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20230206163608(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20230206163608(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
applyNext(t, db)
|
||||
|
||||
stmt := `
|
||||
INSERT INTO
|
||||
mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
|
||||
mcBytes := []byte(`<?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>PayloadContent</key>
|
||||
<array/>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>TestPayloadName</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>TestPayloadIdentifier</string>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>TestPayloadUUID</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
`)
|
||||
|
||||
_, err := db.Exec(stmt, 0, "TestPayloadIdentifier", "TestPayloadName", mcBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
var (
|
||||
identifier string
|
||||
mobileconfig []byte
|
||||
)
|
||||
err = db.QueryRow(`SELECT identifier, mobileconfig FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?`, "TestPayloadName", 0).Scan(&identifier, &mobileconfig)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "TestPayloadIdentifier", identifier)
|
||||
require.Equal(t, mcBytes, mobileconfig)
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,11 +1,17 @@
|
||||
package fleet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/micromdm/nanodep/godep"
|
||||
"github.com/micromdm/nanomdm/mdm"
|
||||
"go.mozilla.org/pkcs7"
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
// MDMAppleEnrollmentType is the type for Apple MDM enrollments.
|
||||
@ -192,3 +198,84 @@ func (e MDMAppleCommandTimeoutError) Error() string {
|
||||
func (e MDMAppleCommandTimeoutError) StatusCode() int {
|
||||
return http.StatusGatewayTimeout
|
||||
}
|
||||
|
||||
// Mobileconfig is the byte slice corresponding to an XML property list (i.e. plist) representation
|
||||
// of an Apple MDM configuration profile in Fleet.
|
||||
//
|
||||
// Configuration profiles are used to configure Apple devices. See also
|
||||
// https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles.
|
||||
type Mobileconfig []byte
|
||||
|
||||
// ParseConfigProfile attempts to parse the Mobileconfig byte slice as a Fleet MDMAppleConfigProfile.
|
||||
//
|
||||
// The byte slice must be XML or PKCS7 parseable. Fleet also requires that it contains both
|
||||
// a PayloadIdentifier and a PayloadDisplayName and that it has PayloadType set to "Configuration".
|
||||
//
|
||||
// Adapted from https://github.com/micromdm/micromdm/blob/main/platform/profile/profile.go
|
||||
func (mc *Mobileconfig) ParseConfigProfile() (*MDMAppleConfigProfile, error) {
|
||||
mcBytes := *mc
|
||||
if !bytes.HasPrefix(mcBytes, []byte("<?xml")) {
|
||||
p7, err := pkcs7.Parse(mcBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mobileconfig is not XML nor PKCS7 parseable: %w", err)
|
||||
}
|
||||
err = p7.Verify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mcBytes = Mobileconfig(p7.Content)
|
||||
}
|
||||
var parsed struct {
|
||||
PayloadIdentifier string
|
||||
PayloadDisplayName string
|
||||
PayloadType string
|
||||
}
|
||||
_, err := plist.Unmarshal(mcBytes, &parsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.PayloadType != "Configuration" {
|
||||
return nil, fmt.Errorf("invalid PayloadType: %s", parsed.PayloadType)
|
||||
}
|
||||
if parsed.PayloadIdentifier == "" {
|
||||
return nil, errors.New("empty PayloadIdentifier in profile")
|
||||
}
|
||||
if parsed.PayloadDisplayName == "" {
|
||||
return nil, errors.New("empty PayloadDisplayName in profile")
|
||||
}
|
||||
|
||||
return &MDMAppleConfigProfile{
|
||||
Identifier: parsed.PayloadIdentifier,
|
||||
Name: parsed.PayloadDisplayName,
|
||||
Mobileconfig: mc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MDMAppleConfigProfile represents an Apple MDM configuration profile in Fleet.
|
||||
// Configuration profiles are used to configure Apple devices .
|
||||
// See also https://developer.apple.com/documentation/devicemanagement/configuring_multiple_devices_using_profiles.
|
||||
type MDMAppleConfigProfile struct {
|
||||
// ProfileID is the unique id of the configuration profile in Fleet
|
||||
ProfileID uint `db:"profile_id"`
|
||||
// TeamID is the id of the team with which the configuration is associated. A team id of zero
|
||||
// represents a configuration profile that is not associated with any team.
|
||||
TeamID uint `db:"team_id"`
|
||||
// Identifier corresponds to the payload identifier of the associated mobileconfig payload.
|
||||
// Fleet requires that Identifier must be unique in combination with the Name and TeamID.
|
||||
Identifier string `db:"identifier"`
|
||||
// Name corresponds to the payload display name of the associated mobileconfig payload.
|
||||
// Fleet requires that Name must be unique in combination with the Identifier and TeamID.
|
||||
Name string `db:"name"`
|
||||
// Mobileconfig is the byte slice corresponding to the XML property list (i.e. plist)
|
||||
// representation of the configuration profile. It must be XML or PKCS7 parseable.
|
||||
Mobileconfig *Mobileconfig `db:"mobileconfig"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
func (cp *MDMAppleConfigProfile) Validate() error {
|
||||
// TODO(sarah): Additional validations for PayloadContent (e.g., screening out FileVault payloads)
|
||||
// should be handled here
|
||||
|
||||
return nil
|
||||
}
|
||||
|
119
server/fleet/apple_mdm_test.go
Normal file
119
server/fleet/apple_mdm_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package fleet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mozilla.org/pkcs7"
|
||||
|
||||
"github.com/micromdm/scep/v2/depot"
|
||||
)
|
||||
|
||||
func TestMDMAppleConfigProfile(t *testing.T) {
|
||||
cases := []struct {
|
||||
testName string
|
||||
mobileconfig Mobileconfig
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
testName: "TestParseConfigProfileOK",
|
||||
mobileconfig: mobileconfigForTest("ValidName", "ValidIdentifier", uuid.NewString()),
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
testName: "TestParseConfigProfileNoIdentifier",
|
||||
mobileconfig: mobileconfigForTest("ValidName", "", uuid.NewString()),
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
testName: "TestParseConfigProfileNoName",
|
||||
mobileconfig: mobileconfigForTest("", "ValidIdentifier", uuid.NewString()),
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
testName: "TestParseConfigProfileNoNameNoIdentifier",
|
||||
mobileconfig: mobileconfigForTest("", "", uuid.NewString()),
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
testName: "TestParseConfigProfileInvalidEncoding",
|
||||
mobileconfig: func() []byte {
|
||||
b, err := json.Marshal(MDMAppleConfigProfile{Name: "ValidName", Identifier: "ValidIdentifier"})
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}(),
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
testName: "TestParseConfigProfilePKCS7Encoding",
|
||||
mobileconfig: func() []byte {
|
||||
// generate certificate for signed data test
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
crtBytes, err := depot.NewCACert().SelfSign(rand.Reader, key.Public(), key)
|
||||
require.NoError(t, err)
|
||||
crt, err := x509.ParseCertificate(crtBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// encode mobileconfig as PKCS7 signed data
|
||||
signedData, err := pkcs7.NewSignedData(mobileconfigForTest("ValidName", "ValidIdentifier", uuid.NewString()))
|
||||
require.NoError(t, err)
|
||||
err = signedData.AddSigner(crt, key, pkcs7.SignerInfoConfig{})
|
||||
require.NoError(t, err)
|
||||
signedBytes, err := signedData.Finish()
|
||||
require.NoError(t, err)
|
||||
p7, err := pkcs7.Parse(signedBytes)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, p7.Verify())
|
||||
|
||||
return signedBytes
|
||||
}(),
|
||||
shouldFail: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.testName, func(t *testing.T) {
|
||||
mc := c.mobileconfig
|
||||
cp := new(MDMAppleConfigProfile)
|
||||
cp.Mobileconfig = &mc
|
||||
|
||||
parsed, err := cp.Mobileconfig.ParseConfigProfile()
|
||||
if c.shouldFail {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ValidName", parsed.Name)
|
||||
require.Equal(t, "ValidIdentifier", parsed.Identifier)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mobileconfigForTest(name string, identifier string, uuid string) Mobileconfig {
|
||||
return []byte(fmt.Sprintf(`<?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>PayloadContent</key>
|
||||
<array/>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>%s</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>%s</string>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>%s</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
`, name, identifier, uuid))
|
||||
}
|
Loading…
Reference in New Issue
Block a user