pre-populate username/fullname during account creation (#11557)

Related to #10744, this pre-populates and disables the username/fullname
fields.

https://user-images.githubusercontent.com/4419992/236854781-ac67ee28-c19c-4130-a5e6-2872220501b5.mov


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Roberto Dip 2023-05-18 12:50:00 -03:00 committed by GitHub
parent b0828e8b88
commit 8e532a5e76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 509 additions and 162 deletions

View File

@ -0,0 +1 @@
* MDM: DEP enrollments configured with SSO now pre-populate the username/fullname fields during account creation.

View File

@ -655,6 +655,7 @@ This is the callback endpoint that the identity provider will use to send securi
If the credentials are valid, the server redirects the client to the Fleet UI. The URL contains the following query parameters that can be used to complete the DEP enrollment flow:
- `enrollment_reference` a reference that must be passed along with `profile_token` to the endpoint to download an enrollment profile.
- `profile_token` is a token that can be used to download an enrollment profile (.mobileconfig).
- `eula_token` (optional) if an EULA was uploaded, this contains a token that can be used to view the EULA document.

View File

@ -9,6 +9,7 @@ import (
"errors"
"io"
"net/url"
"strings"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/authz"
@ -635,6 +636,26 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
return "", ctxerr.Wrap(ctx, err, "validating sso response")
}
// Store information for automatic account population/creation
//
// For now, we just grab whatever comes before the `@` in UserID, which
// must be an email.
//
// For more details, check https://github.com/fleetdm/fleet/issues/10744#issuecomment-1540605146
username, _, found := strings.Cut(auth.UserID(), "@")
if !found {
svc.logger.Log("mdm-sso-callback", "IdP UserID doesn't look like an email, using raw value")
username = auth.UserID()
}
idpAcc := fleet.MDMIdPAccount{
UUID: uuid.New().String(),
Username: username,
Fullname: auth.UserDisplayName(),
}
if err := svc.ds.InsertMDMIdPAccount(ctx, &idpAcc); err != nil {
return "", ctxerr.Wrap(ctx, err, "saving account data from IdP")
}
eula, err := svc.ds.MDMAppleGetEULAMetadata(ctx)
if err != nil && !fleet.IsNotFound(err) {
return "", ctxerr.Wrap(ctx, err, "getting EULA metadata")
@ -650,7 +671,12 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
return "", ctxerr.Wrap(ctx, err, "missing profile")
}
q := url.Values{"profile_token": {depProf.Token}}
q := url.Values{
"profile_token": {depProf.Token},
// using the idp token as a reference just because that's the
// only thing we're referencing later on during enrollment.
"enrollment_reference": {idpAcc.UUID},
}
if eula != nil {
q.Add("eula_token", eula.Token)
}

View File

@ -17,9 +17,14 @@ const RedirectTo = ({ url }: { url: string }) => {
interface IEnrollmentGateProps {
profileToken?: string;
eulaToken?: string;
enrollmentReference?: string;
}
const EnrollmentGate = ({ profileToken, eulaToken }: IEnrollmentGateProps) => {
const EnrollmentGate = ({
profileToken,
eulaToken,
enrollmentReference,
}: IEnrollmentGateProps) => {
const [showEULA, setShowEULA] = useState(Boolean(eulaToken));
if (!profileToken) {
@ -47,22 +52,36 @@ const EnrollmentGate = ({ profileToken, eulaToken }: IEnrollmentGateProps) => {
}
return (
<RedirectTo url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE(profileToken)} />
<RedirectTo
url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE(
profileToken,
enrollmentReference
)}
/>
);
};
interface IMDMSSOCallbackQuery {
eula_token?: string;
profile_token?: string;
enrollment_reference?: string;
}
const MDMAppleSSOCallbackPage = (
props: WithRouterProps<object, IMDMSSOCallbackQuery>
) => {
const { eula_token, profile_token } = props.location.query;
const {
eula_token,
profile_token,
enrollment_reference,
} = props.location.query;
return (
<div className={baseClass}>
<EnrollmentGate eulaToken={eula_token} profileToken={profile_token} />
<EnrollmentGate
eulaToken={eula_token}
profileToken={profile_token}
enrollmentReference={enrollment_reference}
/>
</div>
);
};

View File

@ -50,8 +50,13 @@ export default {
MDM_PROFILES_AGGREGATE_STATUSES: `/${API_VERSION}/fleet/mdm/apple/profiles/summary`,
MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: `/${API_VERSION}/fleet/mdm/apple/filevault/summary`,
MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`,
MDM_APPLE_ENROLLMENT_PROFILE: (token: string) =>
`/api/mdm/apple/enroll?token=${token}`,
MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => {
const query = new URLSearchParams({ token });
if (ref) {
query.append("enrollment_reference", ref);
}
return `/api/mdm/apple/enroll?${query}`;
},
MDM_BOOTSTRAP_PACKAGE_METADATA: (teamId: number) =>
`/${API_VERSION}/fleet/mdm/apple/bootstrap/${teamId}/metadata`,
MDM_BOOTSTRAP_PACKAGE: `/${API_VERSION}/fleet/mdm/apple/bootstrap`,

View File

@ -1591,19 +1591,30 @@ WHERE
func (ds *Datastore) InsertMDMIdPAccount(ctx context.Context, account *fleet.MDMIdPAccount) error {
stmt := `
INSERT INTO mdm_idp_accounts
(uuid, username, salt, entropy, iterations)
(uuid, username, fullname)
VALUES
(?, ?, ?, ?, ?)
(?, ?, ?)
ON DUPLICATE KEY UPDATE
username = VALUES(username),
salt = VALUES(salt),
entropy = VALUES(entropy),
iterations = VALUES(iterations)`
fullname = VALUES(fullname)`
_, err := ds.writer.ExecContext(ctx, stmt, account.UUID, account.Username, account.Salt, account.Entropy, account.Iterations)
_, err := ds.writer.ExecContext(ctx, stmt, account.UUID, account.Username, account.Fullname)
return ctxerr.Wrap(ctx, err, "creating new MDM IdP account")
}
func (ds *Datastore) GetMDMIdPAccount(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname FROM mdm_idp_accounts WHERE uuid = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader, &acct, stmt, uuid)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMIdPAccount").WithMessage(fmt.Sprintf("with uuid %s", uuid)))
}
return nil, ctxerr.Wrap(ctx, err, "select mdm_idp_accounts")
}
return &acct, nil
}
func subqueryDiskEncryptionVerifying() (string, []interface{}) {
sql := `
SELECT

View File

@ -46,7 +46,7 @@ func TestMDMApple(t *testing.T) {
{"TestGetMDMAppleProfilesContents", testGetMDMAppleProfilesContents},
{"TestAggregateMacOSSettingsStatusWithFileVault", testAggregateMacOSSettingsStatusWithFileVault},
{"TestMDMAppleHostsProfilesStatus", testMDMAppleHostsProfilesStatus},
{"TestMDMAppleInsertIdPAccount", testMDMAppleInsertIdPAccount},
{"TestMDMAppleIdPAccount", testMDMAppleIdPAccount},
{"TestIgnoreMDMClientError", testIgnoreMDMClientError},
{"TestDeleteMDMAppleProfilesForHost", testDeleteMDMAppleProfilesForHost},
{"TestBulkSetPendingMDMAppleHostProfiles", testBulkSetPendingMDMAppleHostProfiles},
@ -1812,16 +1812,12 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
}
func testMDMAppleInsertIdPAccount(t *testing.T, ds *Datastore) {
func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) {
ctx := context.Background()
acc := &fleet.MDMIdPAccount{
UUID: "ABC-DEF",
Username: "email@example.com",
SaltedSHA512PBKDF2Dictionary: fleet.SaltedSHA512PBKDF2Dictionary{
Iterations: 50000,
Salt: []byte("salt"),
Entropy: []byte("entropy"),
},
Fullname: "John Doe",
}
err := ds.InsertMDMIdPAccount(ctx, acc)
@ -1831,20 +1827,14 @@ func testMDMAppleInsertIdPAccount(t *testing.T, ds *Datastore) {
err = ds.InsertMDMIdPAccount(ctx, acc)
require.NoError(t, err)
// try to insert an empty account
err = ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{})
require.Error(t, err)
out, err := ds.GetMDMIdPAccount(ctx, acc.UUID)
require.NoError(t, err)
require.Equal(t, acc, out)
// duplicated values get updated
acc.SaltedSHA512PBKDF2Dictionary.Iterations = 3000
acc.SaltedSHA512PBKDF2Dictionary.Salt = []byte("tlas")
acc.SaltedSHA512PBKDF2Dictionary.Entropy = []byte("yportne")
err = ds.InsertMDMIdPAccount(ctx, acc)
require.NoError(t, err)
var out fleet.MDMIdPAccount
err = sqlx.GetContext(ctx, ds.reader, &out, "SELECT * FROM mdm_idp_accounts WHERE uuid = 'ABC-DEF'")
require.NoError(t, err)
require.Equal(t, acc, &out)
var nfe fleet.NotFoundError
out, err = ds.GetMDMIdPAccount(ctx, "BAD-TOKEN")
require.ErrorAs(t, err, &nfe)
require.Nil(t, out)
}
func testIgnoreMDMClientError(t *testing.T, ds *Datastore) {

View File

@ -0,0 +1,28 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20230518114155, Down_20230518114155)
}
func Up_20230518114155(tx *sql.Tx) error {
stmt := `
ALTER TABLE mdm_idp_accounts
DROP COLUMN salt,
DROP COLUMN entropy,
DROP COLUMN iterations,
ADD COLUMN fullname varchar(256) NOT NULL DEFAULT ''
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("alter mdm_idp_accounts table: %w", err)
}
return nil
}
func Down_20230518114155(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,49 @@
package tables
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestUp_20230518114155(t *testing.T) {
db := applyUpToPrev(t)
insertStmt := `
INSERT INTO mdm_idp_accounts
(uuid, username, salt, entropy, iterations)
VALUES
(?, ?, ?, ?, ?)
`
uuidVal := uuid.New().String()
execNoErr(t, db, insertStmt, uuidVal, "test@example.com", "salt", "entropy", 10000)
applyNext(t, db)
// retrieve the stored value
var mdmIdPAccount struct {
UUID string
Username string
Fullname string
}
err := db.Get(&mdmIdPAccount, "SELECT * FROM mdm_idp_accounts WHERE uuid = ?", uuidVal)
require.NoError(t, err)
require.Equal(t, uuidVal, mdmIdPAccount.UUID)
require.Equal(t, "test@example.com", mdmIdPAccount.Username)
require.Equal(t, "", mdmIdPAccount.Fullname)
insertStmt = `
INSERT INTO mdm_idp_accounts
(uuid, username, fullname)
VALUES
(?, ?, ?)
`
uuidVal = uuid.New().String()
execNoErr(t, db, insertStmt, uuidVal, "test+1@example.com", "Foo Bar")
err = db.Get(&mdmIdPAccount, "SELECT * FROM mdm_idp_accounts WHERE uuid = ?", uuidVal)
require.NoError(t, err)
require.Equal(t, uuidVal, mdmIdPAccount.UUID)
require.Equal(t, "test+1@example.com", mdmIdPAccount.Username)
require.Equal(t, "Foo Bar", mdmIdPAccount.Fullname)
}

File diff suppressed because one or more lines are too long

View File

@ -888,6 +888,9 @@ type Datastore interface {
// InsertMDMIdPAccount inserts a new MDM IdP account
InsertMDMIdPAccount(ctx context.Context, account *MDMIdPAccount) error
// GetMDMIdPAccount returns MDM IdP account that matches the given token.
GetMDMIdPAccount(ctx context.Context, uuid string) (*MDMIdPAccount, error)
// GetMDMAppleFileVaultSummary summarizes the current state of Apple disk encryption profiles on
// each macOS host in the specified team (or, if no team is specified, each host that is not assigned
// to any team).

View File

@ -50,19 +50,12 @@ type AppConfigUpdater interface {
SaveAppConfig(ctx context.Context, info *AppConfig) error
}
// SaltedSha512PBKDF2Dictionary is a SHA512 PBKDF2 dictionary.
type SaltedSHA512PBKDF2Dictionary struct {
Iterations int `plist:"iterations"`
Salt []byte `plist:"salt"`
Entropy []byte `plist:"entropy"`
}
// MDMIdPAccount contains account information of a third-party IdP that can be
// later used for MDM operations like creating local accounts.
type MDMIdPAccount struct {
SaltedSHA512PBKDF2Dictionary
UUID string
Username string
Fullname string
}
type MDMAppleBootstrapPackage struct {

View File

@ -604,7 +604,7 @@ type Service interface {
// TODO(mna): this may have to be removed if we don't end up supporting
// manual enrollment via a token (currently we only support it via Fleet
// Desktop, in the My Device page). See #8701.
GetMDMAppleEnrollmentProfileByToken(ctx context.Context, enrollmentToken string) (profile []byte, err error)
GetMDMAppleEnrollmentProfileByToken(ctx context.Context, enrollmentToken string, enrollmentRef string) (profile []byte, err error)
// GetDeviceMDMAppleEnrollmentProfile loads the raw (PList-format) enrollment
// profile for the currently authenticated device.

View File

@ -56,19 +56,32 @@ const (
)
func ResolveAppleMDMURL(serverURL string) (string, error) {
return resolveURL(serverURL, MDMPath)
return resolveURL(serverURL, MDMPath, false)
}
func ResolveAppleEnrollMDMURL(serverURL string) (string, error) {
return resolveURL(serverURL, EnrollPath, false)
}
func ResolveAppleSCEPURL(serverURL string) (string, error) {
return resolveURL(serverURL, SCEPPath)
// Apple's SCEP client appends a query string to the SCEP URL in the
// enrollment profile, without checking if the URL already has a query
// string. Eg: if the URL is `/test/example?foo=bar` it'll make a
// request to `/test/example?foo=bar?SCEPOperation=..`
//
// As a consequence we ensure that the query is always clean for the SCEP URL.
return resolveURL(serverURL, SCEPPath, true)
}
func resolveURL(serverURL, relPath string) (string, error) {
func resolveURL(serverURL, relPath string, cleanQuery bool) (string, error) {
u, err := url.Parse(serverURL)
if err != nil {
return "", err
}
u.Path = path.Join(u.Path, relPath)
if cleanQuery {
u.RawQuery = ""
}
return u.String(), nil
}

View File

@ -171,6 +171,31 @@ func (svc *MDMAppleCommander) InstallEnterpriseApplicationWithEmbeddedManifest(
return svc.EnqueueCommand(ctx, hostUUIDs, string(raw))
}
func (svc *MDMAppleCommander) AccountConfiguration(ctx context.Context, hostUUIDs []string, uuid, fullName, userName string) error {
raw := 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>Command</key>
<dict>
<key>PrimaryAccountFullName</key>
<string>%s</string>
<key>PrimaryAccountUserName</key>
<string>%s</string>
<key>LockPrimaryAccountInfo</key>
<true />
<key>RequestType</key>
<string>AccountConfiguration</string>
</dict>
<key>CommandUUID</key>
<string>%s</string>
</dict>
</plist>`, fullName, userName, uuid)
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
}
// EnqueueCommand takes care of enqueuing the commands and sending push
// notifications to the devices.
//

View File

@ -4,14 +4,12 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/binary"
"encoding/pem"
"errors"
"fmt"
"math"
"math/big"
"net/url"
"path"
"strings"
@ -19,7 +17,6 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/micromdm/nanomdm/mdm"
"golang.org/x/crypto/pbkdf2"
)
// Note Apple rejects CSRs if the key size is not 2048.
@ -104,48 +101,6 @@ func GenerateRandomPin(length int) string {
return fmt.Sprintf(f, v)
}
const pbkdf2KeyLen = 128
// SaltedSHA512PBKDF2 creates a SALTED-SHA512-PBKDF2 dictionary
// from a plaintext password.
//
// This implementation has been taken from micromdm's `password` package
// https://github.com/micromdm/micromdm/blob/974ba0d2060c55dbcf588e832acd89e5b2aa5f41/pkg/crypto/password/password.go#L31-L47
func SaltedSHA512PBKDF2(plaintext string) (fleet.SaltedSHA512PBKDF2Dictionary, error) {
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return fleet.SaltedSHA512PBKDF2Dictionary{}, err
}
iterations, err := secureRandInt(20000, 40000)
if err != nil {
return fleet.SaltedSHA512PBKDF2Dictionary{}, err
}
return fleet.SaltedSHA512PBKDF2Dictionary{
Iterations: iterations,
Salt: salt,
Entropy: pbkdf2.Key([]byte(plaintext), salt, iterations, pbkdf2KeyLen, sha512.New),
}, nil
}
// CCCalibratePBKDF uses a pseudorandom value returned within 100 milliseconds.
// Use a random int from crypto/rand between 20,000 and 40,000 instead.
//
// This implementation has been taken from micromdm's `password` package
func secureRandInt(min, max int64) (int, error) {
var random int
for {
iter, err := rand.Int(rand.Reader, big.NewInt(max))
if err != nil {
return 0, err
}
if iter.Int64() >= min {
random = int(iter.Int64())
break
}
}
return random, nil
}
func FmtErrorChain(chain []mdm.ErrorChain) string {
var sb strings.Builder
for _, mdmErr := range chain {

View File

@ -594,6 +594,8 @@ type GetMDMAppleHostsProfilesSummaryFunc func(ctx context.Context, teamID *uint)
type InsertMDMIdPAccountFunc func(ctx context.Context, account *fleet.MDMIdPAccount) error
type GetMDMIdPAccountFunc func(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error)
type GetMDMAppleFileVaultSummaryFunc func(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error)
type InsertMDMAppleBootstrapPackageFunc func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error
@ -1498,6 +1500,9 @@ type DataStore struct {
InsertMDMIdPAccountFunc InsertMDMIdPAccountFunc
InsertMDMIdPAccountFuncInvoked bool
GetMDMIdPAccountFunc GetMDMIdPAccountFunc
GetMDMIdPAccountFuncInvoked bool
GetMDMAppleFileVaultSummaryFunc GetMDMAppleFileVaultSummaryFunc
GetMDMAppleFileVaultSummaryFuncInvoked bool
@ -3578,6 +3583,13 @@ func (s *DataStore) InsertMDMIdPAccount(ctx context.Context, account *fleet.MDMI
return s.InsertMDMIdPAccountFunc(ctx, account)
}
func (s *DataStore) GetMDMIdPAccount(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
s.mu.Lock()
s.GetMDMIdPAccountFuncInvoked = true
s.mu.Unlock()
return s.GetMDMIdPAccountFunc(ctx, uuid)
}
func (s *DataStore) GetMDMAppleFileVaultSummary(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) {
s.mu.Lock()
s.GetMDMAppleFileVaultSummaryFuncInvoked = true

View File

@ -10,6 +10,7 @@ import (
"io"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
@ -1033,7 +1034,8 @@ func (svc *Service) EnqueueMDMAppleCommand(
}
type mdmAppleEnrollRequest struct {
Token string `query:"token"`
Token string `query:"token"`
EnrollmentReference string `query:"enrollment_reference,optional"`
}
func (r mdmAppleEnrollResponse) error() error { return r.Err }
@ -1063,7 +1065,7 @@ func (r mdmAppleEnrollResponse) hijackRender(ctx context.Context, w http.Respons
func mdmAppleEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*mdmAppleEnrollRequest)
profile, err := svc.GetMDMAppleEnrollmentProfileByToken(ctx, req.Token)
profile, err := svc.GetMDMAppleEnrollmentProfileByToken(ctx, req.Token, req.EnrollmentReference)
if err != nil {
return mdmAppleEnrollResponse{Err: err}, nil
}
@ -1072,7 +1074,7 @@ func mdmAppleEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.
}, nil
}
func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, token string) (profile []byte, err error) {
func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, token string, ref string) (profile []byte, err error) {
// skipauth: The enroll profile endpoint is unauthenticated.
svc.authz.SkipAuthorization(ctx)
@ -1089,11 +1091,21 @@ func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, tok
return nil, ctxerr.Wrap(ctx, err)
}
// TODO(lucas): Actually use enrollment (when we define which configuration we want to define
// on enrollments).
enrollURL := appConfig.ServerSettings.ServerURL
if ref != "" {
u, err := url.Parse(enrollURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "parsing configured server URL")
}
q := u.Query()
q.Add("enroll_reference", ref)
u.RawQuery = q.Encode()
enrollURL = u.String()
}
mobileconfig, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
appConfig.OrgInfo.OrgName,
appConfig.ServerSettings.ServerURL,
enrollURL,
svc.config.MDM.AppleSCEPChallenge,
svc.mdmPushCertTopic,
)
@ -2149,49 +2161,93 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm.
}
if info.InstalledFromDEP {
svc.logger.Log("info", "running post-enroll commands in newly enrolled DEP device", "host_uuid", r.ID)
cmdUUID := uuid.New().String()
if err := svc.commander.InstallEnterpriseApplication(r.Context, []string{m.Enrollment.UDID}, cmdUUID, apple_mdm.FleetdPublicManifestURL); err != nil {
return err
if err := svc.installEnrollmentPackages(r, m, info.TeamID); err != nil {
return fmt.Errorf("installing post-enrollment packages: %w", err)
}
svc.logger.Log("info", "sent command to install fleetd", "host_uuid", r.ID)
meta, err := svc.ds.GetMDMAppleBootstrapPackageMeta(r.Context, info.TeamID)
if err != nil {
var nfe fleet.NotFoundError
if errors.As(err, &nfe) {
svc.logger.Log("info", "unable to find a bootstrap package for DEP enrolled device, skppping installation", "host_uuid", r.ID)
return nil
if ref, ok := r.Params["enroll_reference"]; ok {
svc.logger.Log("info", "got an enroll_reference", "host_uuid", r.ID, "ref", ref)
appCfg, err := svc.ds.AppConfig(r.Context)
if err != nil {
return fmt.Errorf("getting app config: %w", err)
}
return err
acct, err := svc.ds.GetMDMIdPAccount(r.Context, ref)
if err != nil {
return fmt.Errorf("getting idp account details for enroll reference %s: %w", ref, err)
}
ssoEnabled := appCfg.MDM.MacOSSetup.EnableEndUserAuthentication
if info.TeamID != 0 {
team, err := svc.ds.Team(r.Context, info.TeamID)
if err != nil {
return fmt.Errorf("fetch team to send AccountConfiguration: %w", err)
}
ssoEnabled = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
}
if ssoEnabled {
svc.logger.Log("info", "setting username and fullname", "host_uuid", r.ID)
if err := svc.commander.AccountConfiguration(
r.Context,
[]string{m.Enrollment.UDID},
uuid.New().String(),
acct.Fullname,
acct.Username,
); err != nil {
return fmt.Errorf("sending AccountConfiguration command: %w", err)
}
}
}
appCfg, err := svc.ds.AppConfig(r.Context)
if err != nil {
return err
}
url, err := meta.URL(appCfg.ServerSettings.ServerURL)
if err != nil {
return err
}
manifest := appmanifest.NewFromSha(meta.Sha256, url)
cmdUUID = uuid.New().String()
err = svc.commander.InstallEnterpriseApplicationWithEmbeddedManifest(r.Context, []string{m.Enrollment.UDID}, cmdUUID, manifest)
if err != nil {
return err
}
err = svc.ds.RecordHostBootstrapPackage(r.Context, cmdUUID, r.ID)
if err != nil {
return err
}
svc.logger.Log("info", "sent command to install bootstrap package", "host_uuid", r.ID)
}
}
return nil
}
func (svc *MDMAppleCheckinAndCommandService) installEnrollmentPackages(r *mdm.Request, m *mdm.TokenUpdate, teamID uint) error {
cmdUUID := uuid.New().String()
if err := svc.commander.InstallEnterpriseApplication(r.Context, []string{m.Enrollment.UDID}, cmdUUID, apple_mdm.FleetdPublicManifestURL); err != nil {
return err
}
svc.logger.Log("info", "sent command to install fleetd", "host_uuid", r.ID)
meta, err := svc.ds.GetMDMAppleBootstrapPackageMeta(r.Context, teamID)
if err != nil {
var nfe fleet.NotFoundError
if errors.As(err, &nfe) {
svc.logger.Log("info", "unable to find a bootstrap package for DEP enrolled device, skppping installation", "host_uuid", r.ID)
return nil
}
return err
}
appCfg, err := svc.ds.AppConfig(r.Context)
if err != nil {
return err
}
url, err := meta.URL(appCfg.ServerSettings.ServerURL)
if err != nil {
return err
}
manifest := appmanifest.NewFromSha(meta.Sha256, url)
cmdUUID = uuid.New().String()
err = svc.commander.InstallEnterpriseApplicationWithEmbeddedManifest(r.Context, []string{m.Enrollment.UDID}, cmdUUID, manifest)
if err != nil {
return err
}
err = svc.ds.RecordHostBootstrapPackage(r.Context, cmdUUID, r.ID)
if err != nil {
return err
}
svc.logger.Log("info", "sent command to install bootstrap package", "host_uuid", r.ID)
return nil
}
// CheckOut handles MDM [CheckOut][1] requests.
//
// This method is executed after the request has been handled by nanomdm, note

View File

@ -229,7 +229,7 @@ func TestAppleMDMAuthorization(t *testing.T) {
ctx = test.UserContext(ctx, test.UserNoRoles)
_, err := svc.GetMDMAppleInstallerByToken(ctx, "foo")
require.NoError(t, err)
_, err = svc.GetMDMAppleEnrollmentProfileByToken(ctx, "foo")
_, err = svc.GetMDMAppleEnrollmentProfileByToken(ctx, "foo", "")
require.NoError(t, err)
_, err = svc.GetMDMAppleInstallerDetailsByToken(ctx, "foo")
require.NoError(t, err)
@ -995,12 +995,14 @@ func TestMDMTokenUpdate(t *testing.T) {
svc := MDMAppleCheckinAndCommandService{ds: ds, commander: cmdr, logger: kitlog.NewNopLogger()}
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
serverURL := "https://example.com"
installEnterpriseApplicationCalls := 0
commands := map[string]int{}
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
installEnterpriseApplicationCalls++
if _, ok := commands[cmd.Command.RequestType]; !ok {
commands[cmd.Command.RequestType] = 0
}
commands[cmd.Command.RequestType]++
require.NotNil(t, cmd)
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
return nil, nil
}
@ -1030,6 +1032,8 @@ func TestMDMTokenUpdate(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
appCfg := &fleet.AppConfig{}
appCfg.ServerSettings.ServerURL = serverURL
// assign a name to simulate EndUserAuthentication being configured
appCfg.MDM.EndUserAuthentication.IDPName = "FooIdP"
return appCfg, nil
}
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
@ -1053,12 +1057,25 @@ func TestMDMTokenUpdate(t *testing.T) {
require.Equal(t, wantTeamID, teamID)
return &fleet.MDMAppleBootstrapPackage{}, nil
}
ds.RecordHostBootstrapPackageFunc = func(ctx context.Context, commandUUID string, hostUUID string) error {
require.Equal(t, uuid, hostUUID)
require.NotEmpty(t, commandUUID)
return nil
}
idpAcc := &fleet.MDMIdPAccount{
UUID: "FOO-BAR",
Fullname: "Jane Doe",
Username: "jane.doe@example.com",
}
ds.GetMDMIdPAccountFunc = func(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
require.Equal(t, idpAcc.UUID, uuid)
return idpAcc, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
tm := &fleet.Team{}
tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true
return tm, nil
}
err := svc.TokenUpdate(
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
@ -1073,7 +1090,36 @@ func TestMDMTokenUpdate(t *testing.T) {
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
require.True(t, ds.AppConfigFuncInvoked)
require.True(t, ds.RecordHostBootstrapPackageFuncInvoked)
require.Equal(t, 2, installEnterpriseApplicationCalls)
require.False(t, ds.GetMDMIdPAccountFuncInvoked)
require.Equal(t, 2, commands["InstallEnterpriseApplication"])
require.Equal(t, 0, commands["AccountConfiguration"])
ds.BulkSetPendingMDMAppleHostProfilesFuncInvoked = false
ds.GetHostMDMCheckinInfoFuncInvoked = false
ds.AppConfigFuncInvoked = false
ds.RecordHostBootstrapPackageFuncInvoked = false
commands["InstallEnterpriseApplication"] = 0
// with enrollment reference
err = svc.TokenUpdate(
&mdm.Request{
Context: ctx,
EnrollID: &mdm.EnrollID{ID: uuid},
Params: map[string]string{"enroll_reference": idpAcc.UUID},
},
&mdm.TokenUpdate{
Enrollment: mdm.Enrollment{
UDID: uuid,
},
},
)
require.NoError(t, err)
require.True(t, ds.BulkSetPendingMDMAppleHostProfilesFuncInvoked)
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
require.True(t, ds.AppConfigFuncInvoked)
require.True(t, ds.RecordHostBootstrapPackageFuncInvoked)
require.True(t, ds.GetMDMIdPAccountFuncInvoked)
require.Equal(t, 2, commands["InstallEnterpriseApplication"])
require.Equal(t, 1, commands["AccountConfiguration"])
}
func TestMDMCheckout(t *testing.T) {

View File

@ -63,15 +63,16 @@ func TestIntegrationsMDM(t *testing.T) {
type integrationMDMTestSuite struct {
suite.Suite
withServer
fleetCfg config.FleetConfig
fleetDMNextCSRStatus atomic.Value
pushProvider *mock.APNSPushProvider
depStorage nanodep_storage.AllStorage
depSchedule *schedule.Schedule
profileSchedule *schedule.Schedule
onScheduleDone func() // function called when profileSchedule.Trigger() job completed
mdmStorage *mysql.NanoMDMStorage
worker *worker.Worker
fleetCfg config.FleetConfig
fleetDMNextCSRStatus atomic.Value
pushProvider *mock.APNSPushProvider
depStorage nanodep_storage.AllStorage
depSchedule *schedule.Schedule
profileSchedule *schedule.Schedule
onProfileScheduleDone func() // function called when profileSchedule.Trigger() job completed
onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed
mdmStorage *mysql.NanoMDMStorage
worker *worker.Worker
}
func (s *integrationMDMTestSuite) SetupSuite() {
@ -130,6 +131,9 @@ func (s *integrationMDMTestSuite) SetupSuite() {
ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("dep_syncer", func(ctx context.Context) error {
if s.onDEPScheduleDone != nil {
defer s.onDEPScheduleDone()
}
return fleetSyncer.RunAssigner(ctx)
}),
)
@ -144,8 +148,8 @@ func (s *integrationMDMTestSuite) SetupSuite() {
ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("manage_profiles", func(ctx context.Context) error {
if s.onScheduleDone != nil {
defer s.onScheduleDone()
if s.onProfileScheduleDone != nil {
defer s.onProfileScheduleDone()
}
return ReconcileProfiles(ctx, ds, apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService), logger)
}),
@ -374,7 +378,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
triggerSchedule := func() {
ch := make(chan struct{})
s.onScheduleDone = func() {
s.onProfileScheduleDone = func() {
close(ch)
}
_, err := s.profileSchedule.Trigger()
@ -382,6 +386,8 @@ func (s *integrationMDMTestSuite) TestProfileManagement() {
<-ch
}
origPush := s.pushProvider.PushFunc
defer func() { s.pushProvider.PushFunc = origPush }()
s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
require.Len(t, pushes, 1)
require.Equal(t, pushes[0].PushMagic, "pushmagic"+mdmDevice.SerialNumber)
@ -2091,7 +2097,7 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() {
triggerReconcileProfiles := func() {
ch := make(chan bool)
s.onScheduleDone = func() { close(ch) }
s.onProfileScheduleDone = func() { close(ch) }
_, err := s.profileSchedule.Trigger()
require.NoError(t, err)
<-ch
@ -2494,7 +2500,7 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
triggerSchedule := func() {
ch := make(chan bool)
s.onScheduleDone = func() { close(ch) }
s.onProfileScheduleDone = func() { close(ch) }
_, err := s.profileSchedule.Trigger()
require.NoError(t, err)
<-ch
@ -4057,6 +4063,9 @@ func (s *integrationMDMTestSuite) setTokenForTest(t *testing.T, email, password
func (s *integrationMDMTestSuite) TestSSO() {
t := s.T()
mdmDevice := mdmtest.NewTestMDMClientDirect(mdmtest.EnrollInfo{
SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
})
var lastSubmittedProfile *godep.Profile
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@ -4072,9 +4081,38 @@ func (s *integrationMDMTestSuite) TestSSO() {
encoder := json.NewEncoder(w)
err = encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
require.NoError(t, err)
case "/profile/devices":
encoder := json.NewEncoder(w)
err := encoder.Encode(godep.ProfileResponse{
ProfileUUID: "abc",
Devices: map[string]string{},
})
require.NoError(t, err)
case "/server/devices", "/devices/sync":
// This endpoint is used to get an initial list of
// devices, return a single device
encoder := json.NewEncoder(w)
err := encoder.Encode(godep.DeviceResponse{
Devices: []godep.Device{
{
SerialNumber: mdmDevice.SerialNumber,
Model: mdmDevice.Model,
OS: "osx",
OpType: "added",
},
},
})
require.NoError(t, err)
}
}))
// sync the list of ABM devices
ch := make(chan bool)
s.onDEPScheduleDone = func() { close(ch) }
_, err := s.depSchedule.Trigger()
require.NoError(t, err)
<-ch
// MDM SSO fields are empty by default
acResp := appConfigResponse{}
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
@ -4206,11 +4244,18 @@ func (s *integrationMDMTestSuite) TestSSO() {
u, err := url.Parse(res.Header.Get("Location"))
require.NoError(t, err)
q := u.Query()
// without an EULA uploaded, only the profile token is provided
// without an EULA uploaded
require.False(t, q.Has("eula_token"))
require.True(t, q.Has("profile_token"))
require.True(t, q.Has("enrollment_reference"))
// the url retrieves a valid profile
s.downloadAndVerifyEnrollmentProfile("/api/mdm/apple/enroll?token=" + q.Get("profile_token"))
s.downloadAndVerifyEnrollmentProfile(
fmt.Sprintf(
"/api/mdm/apple/enroll?token=%s&enrollment_reference=%s",
q.Get("profile_token"),
q.Get("enrollment_reference"),
),
)
// upload an EULA
pdfBytes := []byte("%PDF-1.pdf-contents")
@ -4223,11 +4268,18 @@ func (s *integrationMDMTestSuite) TestSSO() {
u, err = url.Parse(res.Header.Get("Location"))
require.NoError(t, err)
q = u.Query()
// with an EULA uploaded, both values are present
// with an EULA uploaded, all values are present
require.True(t, q.Has("eula_token"))
require.True(t, q.Has("profile_token"))
require.True(t, q.Has("enrollment_reference"))
// the url retrieves a valid profile
s.downloadAndVerifyEnrollmentProfile("/api/mdm/apple/enroll?token=" + q.Get("profile_token"))
prof := s.downloadAndVerifyEnrollmentProfile(
fmt.Sprintf(
"/api/mdm/apple/enroll?token=%s&enrollment_reference=%s",
q.Get("profile_token"),
q.Get("enrollment_reference"),
),
)
// the url retrieves a valid EULA
resp := s.DoRaw("GET", "/api/latest/fleet/mdm/apple/setup/eula/"+q.Get("eula_token"), nil, http.StatusOK)
require.EqualValues(t, len(pdfBytes), resp.ContentLength)
@ -4236,6 +4288,45 @@ func (s *integrationMDMTestSuite) TestSSO() {
require.NoError(t, err)
require.EqualValues(t, pdfBytes, respBytes)
enrollURL := ""
scepURL := ""
for _, p := range prof.PayloadContent {
switch p.PayloadType {
case "com.apple.security.scep":
scepURL = p.PayloadContent.URL
case "com.apple.mdm":
enrollURL = p.ServerURL
}
}
require.NotEmpty(t, enrollURL)
require.NotEmpty(t, scepURL)
// enroll the device using the provided profile
// we're using localhost for SSO because that's how the local
// SimpleSAML server is configured, and s.server.URL changes between
// test runs.
mdmDevice.EnrollInfo.MDMURL = strings.Replace(enrollURL, "https://localhost:8080", s.server.URL, 1)
mdmDevice.EnrollInfo.SCEPURL = strings.Replace(scepURL, "https://localhost:8080", s.server.URL, 1)
err = mdmDevice.Enroll()
require.NoError(t, err)
// ask for commands and verify that we get AccountConfiguration
var accCmd *micromdm.CommandPayload
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
if cmd.Command.RequestType == "AccountConfiguration" {
accCmd = cmd
}
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
require.NotNil(t, accCmd)
require.NotNil(t, accCmd.Command)
require.True(t, accCmd.Command.AccountConfiguration.LockPrimaryAccountInfo)
require.Equal(t, "SSO User 1", accCmd.Command.AccountConfiguration.PrimaryAccountFullName)
require.Equal(t, "sso_user", accCmd.Command.AccountConfiguration.PrimaryAccountUserName)
// changing the server URL also updates the remote DEP profile
acResp = appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
@ -4247,7 +4338,22 @@ func (s *integrationMDMTestSuite) TestSSO() {
require.Equal(t, "https://example.com/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)
}
func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string) {
type scepPayload struct {
URL string
}
type enrollmentPayload struct {
PayloadType string
ServerURL string // used by the enrollment payload
PayloadContent scepPayload // scep contains a nested payload content dict
}
type enrollmentProfile struct {
PayloadIdentifier string
PayloadContent []enrollmentPayload
}
func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string) *enrollmentProfile {
t := s.T()
resp := s.DoRaw("GET", path, nil, http.StatusOK)
@ -4263,11 +4369,21 @@ func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string
headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
require.NoError(t, err)
require.Equal(t, len(body), headerLen)
var profile struct {
PayloadIdentifier string `plist:"PayloadIdentifier"`
}
var profile enrollmentProfile
require.NoError(t, plist.Unmarshal(body, &profile))
require.Equal(t, apple_mdm.FleetPayloadIdentifier, profile.PayloadIdentifier)
for _, p := range profile.PayloadContent {
switch p.PayloadType {
case "com.apple.security.scep":
require.NotEmpty(t, p.PayloadContent.URL)
case "com.apple.mdm":
require.NotEmpty(t, p.ServerURL)
default:
require.Failf(t, "unrecognized payload type in enrollment profile: %s", p.PayloadType)
}
}
return &profile
}
func (s *integrationMDMTestSuite) TestDesktopMDMMigration() {