Revert changes to DEP enrollment flow (#20506)

Reverts #20162 and #20355 per [QA
findings](https://github.com/fleetdm/fleet/issues/19185#issuecomment-2229544188)
This commit is contained in:
Sarah Gillespie
2024-07-16 11:44:48 -05:00
committed by GitHub
parent b700ad40ef
commit 7dda2e9f84
28 changed files with 88 additions and 928 deletions
-4
View File
@@ -1,4 +0,0 @@
- Fixed bug where MDM devices would fail to renew enrollment profiles if applicable end user authentication
settings changed after the device was enrolled.
- Improved device user account creation during MDM IdP enrollment flow by removing enrollment
reference from MDM server url in Fleet-generated enrollment profiles.
-2
View File
@@ -942,8 +942,6 @@ the way that the Fleet server works.
"get_frontend",
service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger),
)
frontendHandler = service.WithDEPWebviewRedirect(svc, logger, frontendHandler, config.Server.URLPrefix)
apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore)
setupRequired, err := svc.SetupRequired(baseCtx)
@@ -29,8 +29,6 @@ const EnrollmentGate = ({
}: IEnrollmentGateProps) => {
const [showEULA, setShowEULA] = useState(Boolean(eulaToken));
const dep_device_info = localStorage.getItem("dep_device_info") || "";
if (!profileToken || error) {
return <SSOError />;
}
@@ -57,11 +55,10 @@ const EnrollmentGate = ({
return (
<RedirectTo
url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE({
token: profileToken,
ref: enrollmentReference,
dep_device_info,
})}
url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE(
profileToken,
enrollmentReference
)}
/>
);
};
@@ -1,9 +1,8 @@
import React from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
import { WithRouterProps } from "react-router";
import mdmAPI, { IMDMSSOParams } from "services/entities/mdm";
import mdmAPI from "services/entities/mdm";
import SSOError from "components/MDM/SSOError";
import Spinner from "components/Spinner/Spinner";
@@ -11,15 +10,10 @@ import { IMdmSSOReponse } from "interfaces/mdm";
const baseClass = "mdm-apple-sso-page";
const DEPSSOLoginPage = ({
location: { query },
}: WithRouterProps<object, IMDMSSOParams>) => {
const { dep_device_info } = query;
localStorage.setItem("dep_device_info", dep_device_info || "");
const { error } = useQuery<IMdmSSOReponse, AxiosError>(
const DEPSSOLoginPage = () => {
const { error } = useQuery<void, AxiosError, IMdmSSOReponse>(
["dep_sso"],
() => mdmAPI.initiateMDMAppleSSO(query),
() => mdmAPI.initiateMDMAppleSSO(),
{
retry: false,
refetchOnWindowFocus: false,
+2 -13
View File
@@ -4,7 +4,6 @@ import {
DiskEncryptionStatus,
IHostMdmProfile,
IMdmProfile,
IMdmSSOReponse,
MdmProfileStatus,
} from "interfaces/mdm";
import { API_NO_TEAM_ID } from "interfaces/team";
@@ -68,16 +67,6 @@ export interface IAppleSetupEnrollmentProfileResponse {
enrollment_profile: Record<string, unknown>;
}
export interface IMDMSSOParams {
dep_device_info: string;
}
export interface IMDMAppleEnrollmentProfileParams {
token: string;
ref?: string;
dep_device_info?: string;
}
const mdmService = {
resetEncryptionKey: (token: string) => {
const { DEVICE_USER_RESET_ENCRYPTION_KEY } = endpoints;
@@ -192,9 +181,9 @@ const mdmService = {
});
},
initiateMDMAppleSSO: (params: IMDMSSOParams): Promise<IMdmSSOReponse> => {
initiateMDMAppleSSO: () => {
const { MDM_APPLE_SSO } = endpoints;
return sendRequest("POST", MDM_APPLE_SSO, params);
return sendRequest("POST", MDM_APPLE_SSO, {});
},
getBootstrapPackageMetadata: (teamId: number) => {
+4 -9
View File
@@ -1,5 +1,3 @@
import { IMDMAppleEnrollmentProfileParams } from "services/entities/mdm";
const API_VERSION = "latest";
export default {
@@ -95,14 +93,11 @@ export default {
MDM_PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/mdm/profiles/summary`,
MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`,
MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`,
MDM_APPLE_ENROLLMENT_PROFILE: ({
token,
ref,
dep_device_info,
}: IMDMAppleEnrollmentProfileParams) => {
MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => {
const query = new URLSearchParams({ token });
ref && query.append("enrollment_reference", ref);
dep_device_info && query.append("dep_device_info", dep_device_info);
if (ref) {
query.append("enrollment_reference", ref);
}
return `/api/mdm/apple/enroll?${query}`;
},
MDM_APPLE_SETUP_ENROLLMENT_PROFILE: `/${API_VERSION}/fleet/mdm/apple/enrollment_profile`,
+5 -50
View File
@@ -2732,13 +2732,8 @@ func (ds *Datastore) InsertMDMIdPAccount(ctx context.Context, account *fleet.MDM
return ctxerr.Wrap(ctx, err, "creating new MDM IdP account")
}
func (ds *Datastore) AssociateMDMIdPAccount(ctx context.Context, accountUUID, hostUUID string) error {
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE mdm_idp_accounts SET host_uuid = ? WHERE uuid = ?`, hostUUID, accountUUID)
return ctxerr.Wrap(ctx, err, "associating MDM IdP account with device")
}
func (ds *Datastore) GetMDMIdPAccountByEmail(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname, email, host_uuid, fleet_enroll_ref FROM mdm_idp_accounts WHERE email = ?`
stmt := `SELECT uuid, username, fullname, email FROM mdm_idp_accounts WHERE email = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, email)
if err != nil {
@@ -2750,39 +2745,13 @@ func (ds *Datastore) GetMDMIdPAccountByEmail(ctx context.Context, email string)
return &acct, nil
}
func (ds *Datastore) GetMDMIdPAccountByAccountUUID(ctx context.Context, accountUUID string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname, email, host_uuid, fleet_enroll_ref FROM mdm_idp_accounts WHERE uuid = ?`
func (ds *Datastore) GetMDMIdPAccountByUUID(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname, email FROM mdm_idp_accounts WHERE uuid = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, accountUUID)
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, uuid)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMIdPAccount").WithMessage(fmt.Sprintf("with uuid %s", accountUUID)))
}
return nil, ctxerr.Wrap(ctx, err, "select mdm_idp_accounts")
}
return &acct, nil
}
func (ds *Datastore) GetMDMIdPAccountByHostUUID(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname, email, host_uuid, fleet_enroll_ref FROM mdm_idp_accounts WHERE host_uuid = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, hostUUID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMIdPAccount").WithMessage(fmt.Sprintf("with host uuid %s", hostUUID)))
}
return nil, ctxerr.Wrap(ctx, err, "select mdm_idp_accounts")
}
return &acct, nil
}
func (ds *Datastore) GetMDMIdPAccountByLegacyEnrollRef(ctx context.Context, ref string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname, email, host_uuid, fleet_enroll_ref FROM mdm_idp_accounts WHERE fleet_enroll_ref = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, ref)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("MDMIdPAccount").WithMessage(fmt.Sprintf("with fleet_enroll_ref %s", ref)))
return nil, ctxerr.Wrap(ctx, notFound("MDMIdPAccount").WithMessage(fmt.Sprintf("with uuid %s", uuid)))
}
return nil, ctxerr.Wrap(ctx, err, "select mdm_idp_accounts")
}
@@ -3676,20 +3645,6 @@ func (ds *Datastore) MDMResetEnrollment(ctx context.Context, hostUUID string) er
return ctxerr.Wrap(ctx, err, "resetting disk encryption key information for host")
}
// Delete any stored host emails sourced from mdm_idp_accounts. Note that we aren't deleting
// the mdm_idp_accounts themselves, just the host_emails associated with the host. This
// ensures that hosts that reenroll without IdP will have their emails removed. Hosts
// that reenroll with IdP will have their emails re-added in the
// AppleMDMPostDEPEnrollmentTask.
//
// TODO: Should we be applying any platform check here or is this ok for macOS, iOS, and Windows?
_, err = tx.ExecContext(ctx, `
DELETE FROM host_emails
WHERE host_id = ? AND source = ?`, host.ID, fleet.DeviceMappingMDMIdpAccounts)
if err != nil {
return ctxerr.Wrap(ctx, err, "resetting host_emails sourced from mdm_idp_accounts")
}
if host.Platform == "darwin" {
// Deleting the matching entry on this table will cause
// the aggregate report to show this host as 'pending' to
+2 -2
View File
@@ -2470,11 +2470,11 @@ func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) {
require.ErrorAs(t, err, &nfe)
require.Nil(t, out)
out, err = ds.GetMDMIdPAccountByAccountUUID(ctx, acc.UUID)
out, err = ds.GetMDMIdPAccountByUUID(ctx, acc.UUID)
require.NoError(t, err)
require.Equal(t, acc, out)
out, err = ds.GetMDMIdPAccountByAccountUUID(ctx, "BAD-TOKEN")
out, err = ds.GetMDMIdPAccountByUUID(ctx, "BAD-TOKEN")
require.ErrorAs(t, err, &nfe)
require.Nil(t, out)
}
+9 -68
View File
@@ -3629,78 +3629,19 @@ func (ds *Datastore) SetOrUpdateMDMData(
)
}
func (ds *Datastore) SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRef(
func (ds *Datastore) SetOrUpdateHostEmailsFromMdmIdpAccounts(
ctx context.Context,
hostID uint,
fleetEnrollmentRef string,
) error {
if fleetEnrollmentRef == "" {
return ctxerr.New(ctx, "missing fleet enroll ref to upsert host emails with mdm idp account")
}
var email *string
idp, err := ds.GetMDMIdPAccountByLegacyEnrollRef(ctx, fleetEnrollmentRef)
if err != nil {
return err
if fleetEnrollmentRef != "" {
idp, err := ds.GetMDMIdPAccountByUUID(ctx, fleetEnrollmentRef)
if err != nil {
return err
}
email = &idp.Email
}
email = &idp.Email
return ds.updateOrInsert(
ctx,
`UPDATE host_emails SET email = ? WHERE host_id = ? AND source = ?`,
`INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)`,
email, hostID, fleet.DeviceMappingMDMIdpAccounts,
)
}
func (ds *Datastore) SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUID(
ctx context.Context,
hostUUID string,
) error {
if hostUUID == "" {
return ctxerr.New(ctx, "missing host uuid to upsert host emails with mdm idp account")
}
var hid uint
var email string
host, err := ds.HostLiteByIdentifier(ctx, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting host by identifier to upsert host emails with mdm idp account")
}
hid = host.ID
idp, err := ds.GetMDMIdPAccountByHostUUID(ctx, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting idp account by host uuid to upsert host emails with mdm idp account")
}
email = idp.Email
return ds.updateOrInsert(
ctx,
`UPDATE host_emails SET email = ? WHERE host_id = ? AND source = ?`,
`INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)`,
email, hid, fleet.DeviceMappingMDMIdpAccounts,
)
}
func (ds *Datastore) SetOrUpdateEmailsFromMDMIdPAccountsByHostID(
ctx context.Context,
hostID uint,
hostUUID string,
) error {
if hostID == 0 {
return ctxerr.New(ctx, "missing host id to upsert host emails with mdm idp account")
}
if hostUUID == "" {
return ctxerr.New(ctx, "missing host uuid to upsert host emails with mdm idp account")
}
var email string
idp, err := ds.GetMDMIdPAccountByHostUUID(ctx, hostUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting idp account by host uuid to upsert host emails with mdm idp account")
}
email = idp.Email
return ds.updateOrInsert(
ctx,
@@ -5194,8 +5135,8 @@ func (ds *Datastore) loadHostLite(ctx context.Context, id *uint, identifier *str
SELECT
h.id,
h.team_id,
COALESCE(h.osquery_host_id, '') AS osquery_host_id,
COALESCE(h.node_key, '') AS node_key,
h.osquery_host_id,
h.node_key,
h.hostname,
h.uuid,
h.hardware_serial,
-1
View File
@@ -7623,7 +7623,6 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
// compare only the fields we care about
h.CreatedAt = returned.CreatedAt
h.UpdatedAt = returned.UpdatedAt
h.LastEnrolledAt = returned.LastEnrolledAt // FIXME: this seems to be flaky (off by one second) in CI so don't compare it
h.DEPAssignedToFleet = ptr.Bool(false)
assert.Equal(t, h, returned)
}
@@ -1,42 +0,0 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20240709175341, Down_20240709175341)
}
func Up_20240709175341(tx *sql.Tx) error {
alterStmt := `
ALTER TABLE mdm_idp_accounts ADD COLUMN (
host_uuid varchar(63) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
fleet_enroll_ref varchar(63) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''
)`
if _, err := tx.Exec(alterStmt); err != nil {
return fmt.Errorf("failed to alter mdm_idp_accounts table: %w", err)
}
updateStmt := `
UPDATE
mdm_idp_accounts mia
LEFT JOIN host_mdm hmdm ON mia.uuid = hmdm.fleet_enroll_ref
LEFT JOIN hosts h ON hmdm.host_id = h.id
SET
mia.fleet_enroll_ref = mia.uuid,
mia.host_uuid = COALESCE(h.uuid, ''),
mia.updated_at = mia.updated_at`
if _, err := tx.Exec(updateStmt); err != nil {
return fmt.Errorf("failed to update data in mdm_idp_accounts: %w", err)
}
return nil
}
func Down_20240709175341(tx *sql.Tx) error {
return nil
}
@@ -1,74 +0,0 @@
package tables
import (
"context"
"fmt"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
)
func TestUp_20240709175341(t *testing.T) {
db := applyUpToPrev(t)
then := time.Now().UTC().Add(-time.Hour).Round(time.Second)
for i := 0; i < 4; i++ {
// create some hosts with uuids that will be used in migration to populate new columns in mdm_idp_accounts
id := execNoErrLastID(t, db, `INSERT INTO hosts (uuid) VALUES (?);`,
fmt.Sprintf("host_uuid%d", i),
)
// insert host_mdm records that will be used in migration to populate new columns in mdm_idp_accounts
execNoErr(t, db, `INSERT INTO host_mdm (host_id, fleet_enroll_ref) VALUES (?, ?);`,
id, fmt.Sprintf("uuid%d", i),
)
execNoErr(t, db, `
INSERT INTO
mdm_idp_accounts (uuid, username, fullname, email, created_at, updated_at)
VALUES
(?,?,?,?,?,?)
`, fmt.Sprintf("uuid%d", i), fmt.Sprintf("username%d", i), fmt.Sprintf("fullname%d", i), fmt.Sprintf("email%d", i), then, then)
}
// insert an orphaned mdm_idp_account
execNoErr(t, db, `
INSERT INTO
mdm_idp_accounts (uuid, username, fullname, email, created_at, updated_at)
VALUES
(?,?,?,?,?,?)`, "uuid4", "username4", "fullname4", "email4", then, then)
// Apply current migration.
applyNext(t, db)
var dest []struct {
fleet.MDMIdPAccount
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
require.NoError(t, sqlx.SelectContext(context.Background(), db, &dest, `SELECT * FROM mdm_idp_accounts ORDER BY uuid;`))
require.Len(t, dest, 5)
for i, got := range dest {
require.Equal(t, fmt.Sprintf("uuid%d", i), got.UUID) // no change
require.Equal(t, fmt.Sprintf("username%d", i), got.Username) // no change
require.Equal(t, fmt.Sprintf("fullname%d", i), got.Fullname) // no change
require.Equal(t, fmt.Sprintf("email%d", i), got.Email) // no change
require.Equal(t, then, got.CreatedAt) // no change
require.Equal(t, then, got.UpdatedAt) // no change
if i == 4 {
// this is the orphaned mdm_idp_account
require.Equal(t, "uuid4", got.FleetEnrollRef) // new column fleet_enroll_ref is set to uuid in migration
require.Empty(t, got.HostUUID) // new column host_uuid is not set in migration because there is no matching host
} else {
require.Equal(t, fmt.Sprintf("host_uuid%d", i), got.HostUUID) // new column host_uuid is set to host uuid in migration
require.Equal(t, fmt.Sprintf("uuid%d", i), got.FleetEnrollRef) // new column fleet_enroll_ref is set to uuid in migration
}
}
}
@@ -1,25 +0,0 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20240710152744, Down_20240710152744)
}
func Up_20240710152744(tx *sql.Tx) error {
_, err := tx.Exec(`
ALTER TABLE mdm_idp_accounts
ADD INDEX idx_mdm_idp_accounts_host_uuid (host_uuid)`,
)
if err != nil {
return fmt.Errorf("failed to add idx_mdm_idp_accounts_host_uuid: %w", err)
}
return nil
}
func Down_20240710152744(tx *sql.Tx) error {
return nil
}
File diff suppressed because one or more lines are too long
+5 -35
View File
@@ -861,26 +861,9 @@ type Datastore interface {
SetOrUpdateMunkiInfo(ctx context.Context, hostID uint, version string, errors, warnings []string) error
SetOrUpdateMDMData(ctx context.Context, hostID uint, isServer, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollRef string) error
// SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRef sets or updates the host emails associated with the provided
// host based on the MDM IdP account information associated with the provided fleet enrollment
// reference.
//
// Deprecated: Use SetOrUpdateHostEmailsFromMdmIdpAccountsByHostUUID instead.
SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRef(ctx context.Context, hostID uint, fleetEnrollmentRef string) error
// SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUID sets or updates the host emails associated
// with the provided host uuid based on the MDM IdP account information (if any) associated with the
// provided host uuid.
//
// Note: Use this method only if the host ID is not available to the caller, otherwise use
// SetOrUpdateHostEmailsFromMdmIdpAccountsByHostID.
SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUID(ctx context.Context, hostUUID string) error
// SetOrUpdateEmailsFromMDMIdPAccountsByHostID sets or updates the host emails associated with
// the provided host ID based on the MDM IdP account information (if any) associated with the
// provided host UUID.
//
// Note: Use this method when both the host ID and host UUID are known to the caller, otherwise
// use SetOrUpdateHostEmailsFromMdmIdpAccountsByHostUUID.
SetOrUpdateEmailsFromMDMIdPAccountsByHostID(ctx context.Context, hostID uint, hostUUID string) error
// SetOrUpdateHostEmailsFromMdmIdpAccounts sets or updates the host emails associated with the provided
// host based on the MDM IdP account information associated with the provided fleet enrollment reference.
SetOrUpdateHostEmailsFromMdmIdpAccounts(ctx context.Context, hostID uint, fleetEnrollmentRef string) error
SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error
SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error
// SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for
@@ -1187,21 +1170,8 @@ type Datastore interface {
// InsertMDMIdPAccount inserts a new MDM IdP account
InsertMDMIdPAccount(ctx context.Context, account *MDMIdPAccount) error
// AssociateMDMIdPAccount adds device info to an existing MDM IdP account
AssociateMDMIdPAccount(ctx context.Context, accountUUID string, deviceUUID string) error
// GetMDMIdPAccountByAccountUUID returns MDM IdP account that matches the given account uuid.
GetMDMIdPAccountByAccountUUID(ctx context.Context, accountUUID string) (*MDMIdPAccount, error)
// GetMDMIdPAccountByHostUUID returns MDM IdP account that matches the given host uuid.
GetMDMIdPAccountByHostUUID(ctx context.Context, hostUUID string) (*MDMIdPAccount, error)
// GetMDMIdPAccountByLegacyEnrollRef returns MDM IdP account that matches the given Fleet
// enrollment ref.
//
// Deprecated: This method is deprecated and only used for backwards compatibility.
// GetMDMIdPAccountByAccountUUID and GetMDMIdPAccountByDeviceUUID are the preferred methods.
GetMDMIdPAccountByLegacyEnrollRef(ctx context.Context, ref string) (*MDMIdPAccount, error)
// GetMDMIdPAccountByUUID returns MDM IdP account that matches the given token.
GetMDMIdPAccountByUUID(ctx context.Context, uuid string) (*MDMIdPAccount, error)
// GetMDMIdPAccountByEmail returns MDM IdP account that matches the given email.
GetMDMIdPAccountByEmail(ctx context.Context, email string) (*MDMIdPAccount, error)
-12
View File
@@ -66,22 +66,10 @@ type AppConfigUpdater interface {
// MDMIdPAccount contains account information of a third-party IdP that can be
// later used for MDM operations like creating local accounts.
type MDMIdPAccount struct {
// UUID is the unique identifier created when a new user email is ingested (e.g., from the IdP response
// payload during the DEP automatic enrollment flow). It is used to subsequently associate the
// IdP account info to the device UUID extracted from the DEP webview client request.
UUID string
Username string
Fullname string
Email string
// HostUUID is the unique device identifier associated with the MDM enrollment. For Apple
// devices, it corresponds to the UDID extracted from the `x-apple-aspen-deviceinfo` header of
// the DEP webview client request.
HostUUID string `db:"host_uuid"`
// FleetEnrollRef is a legacy reference that is preserved for devices that enrolled
// via a mobileconfig that included an enrollment reference query param in the service URL. It
// is preserved for backwards compatibility with existing enrollments because Apple requires
// server URLs to match exactly when re-enrolling (e.g., via `profiles renew -type enrollment`).
FleetEnrollRef string `db:"fleet_enroll_ref"`
}
type MDMAppleBootstrapPackage struct {
+1 -1
View File
@@ -749,7 +749,7 @@ type Service interface {
GetMDMAppleProfilesSummary(ctx context.Context, teamID *uint) (*MDMProfilesSummary, error)
// GetMDMAppleEnrollmentProfileByToken returns the Apple enrollment from its secret token.
GetMDMAppleEnrollmentProfileByToken(ctx context.Context, enrollmentToken string, enrollmentRef string, deviceinfo 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.
Binary file not shown.
-173
View File
@@ -1,173 +0,0 @@
// The contents of this file have been copied and modified pursuant to the following
// license from the original source:
// https://github.com/korylprince/dep-webview-oidc/blob/2dd846a54fed04c16dd227b8c6c31665b4d0ebd8/header/header.go
//
// MIT License
//
// Copyright (c) 2023 Kory Prince
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package apple_mdm
import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/sha1" // nolint:gosec // See comments regarding Apple's Root CA below
"crypto/x509"
_ "embed"
"encoding/base64"
"errors"
"fmt"
"github.com/groob/plist"
"go.mozilla.org/pkcs7"
)
const DeviceInfoHeader = "x-apple-aspen-deviceinfo"
// appleRootCert is https://www.apple.com/appleca/AppleIncRootCertificate.cer
//
//go:embed AppleIncRootCertificate.cer
var appleRootCert []byte
func newAppleRootCert() *x509.Certificate {
cert, err := x509.ParseCertificate(appleRootCert)
if err != nil {
panic(fmt.Errorf("could not parse cert: %w", err))
}
return cert
}
// appleRootCA is Apple's Root CA parsed to an *x509.Certificate
var appleRootCA = newAppleRootCert()
// MachineInfo is a [device's information] sent as part of an MDM enrollment profile request
//
// [device's information]: https://developer.apple.com/documentation/devicemanagement/machineinfo
type MachineInfo struct {
IMEI string `plist:"IMEI,omitempty"`
Language string `plist:"LANGUAGE,omitempty"`
MEID string `plist:"MEID,omitempty"`
Product string `plist:"PRODUCT"`
Serial string `plist:"SERIAL"`
UDID string `plist:"UDID"`
Version string `plist:"VERSION"`
}
// verifyPKCS7SHA1RSA performs a manual SHA1withRSA verification, since it's deprecated in Go 1.18.
// If verifyChain is true, the signer certificate and its chain of certificates is verified against Apple's Root CA.
// Also note that the certificate validity time window of the signing cert is not checked, since the cert is expired.
// This follows guidance from Apple on the expired certificate.
func verifyPKCS7SHA1RSA(p7 *pkcs7.PKCS7, verifyChain bool) error {
if len(p7.Signers) == 0 {
return errors.New("not signed")
}
// get signing cert
issuer := p7.Signers[0].IssuerAndSerialNumber
var signer *x509.Certificate
for _, cert := range p7.Certificates {
if bytes.Equal(cert.RawIssuer, issuer.IssuerName.FullBytes) && cert.SerialNumber.Cmp(issuer.SerialNumber) == 0 {
signer = cert
}
}
// get sha1 hash of content
hashed := sha1.Sum(p7.Content) // nolint:gosec
// verify content signature
signature := p7.Signers[0].EncryptedDigest
if err := rsa.VerifyPKCS1v15(signer.PublicKey.(*rsa.PublicKey), crypto.SHA1, hashed[:], signature); err != nil {
return fmt.Errorf("signature could not be verified: %w", err)
}
if !verifyChain {
return nil
}
// verify chain from signer to root
cert := signer
outer:
for {
// check if cert is signed by root
if bytes.Equal(cert.RawIssuer, appleRootCA.RawSubject) {
hashed := sha1.Sum(cert.RawTBSCertificate) // nolint:gosec
// check signature
if err := rsa.VerifyPKCS1v15(appleRootCA.PublicKey.(*rsa.PublicKey), crypto.SHA1, hashed[:], cert.Signature); err != nil {
return fmt.Errorf("could not verify root CA signature: %w", err)
}
return nil
}
for _, c := range p7.Certificates {
if cert == c {
continue
}
// check if cert is signed by intermediate cert in chain
if bytes.Equal(cert.RawIssuer, c.RawSubject) {
// check signature
hashed := sha1.Sum(cert.RawTBSCertificate) // nolint:gosec
if err := rsa.VerifyPKCS1v15(c.PublicKey.(*rsa.PublicKey), crypto.SHA1, hashed[:], cert.Signature); err != nil {
return fmt.Errorf("could not verify chained certificate signature: %w", err)
}
cert = c
continue outer
}
}
return errors.New("certificate root not found")
}
}
// ParseDeviceinfo attempts to parse the provided string, assuming it to be the base64-encoded value
// of an x-apple-aspen-deviceinfo header. If successful, it returns the parsed *MachineInfo. If the
// verify parameter is specified as true, the signature is also verified against Apple's Root CA and
// an error will be returned if the signature is invalid.
//
// Warning: The information in this header, despite being signed by Apple PKI, shouldn't be trusted
// for device attestation or other security purposes. See the related [documentation] and referenced
// [article] for more information.
//
// [documentation]: https://github.com/korylprince/dep-webview-oidc/blob/2dd846a54fed04c16dd227b8c6c31665b4d0ebd8/docs/Architecture.md#x-apple-aspen-deviceinfo-header
// [article]: https://duo.com/labs/research/mdm-me-maybe
func ParseDeviceinfo(b64 string, verify bool) (*MachineInfo, error) {
buf, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, fmt.Errorf("could not decode base64: %w", err)
}
p7, err := pkcs7.Parse(buf)
if err != nil {
return nil, fmt.Errorf("could not decode pkcs7: %w", err)
}
// verify signature and certificate chain
if verify {
if err = verifyPKCS7SHA1RSA(p7, verify); err != nil {
return nil, fmt.Errorf("could not verify signature: %w", err)
}
}
info := new(MachineInfo)
if err = plist.Unmarshal(p7.Content, info); err != nil {
return nil, fmt.Errorf("could not decode plist: %w", err)
}
return info, nil
}
+12 -72
View File
@@ -601,11 +601,7 @@ type SetOrUpdateMunkiInfoFunc func(ctx context.Context, hostID uint, version str
type SetOrUpdateMDMDataFunc func(ctx context.Context, hostID uint, isServer bool, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollRef string) error
type SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error
type SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUIDFunc func(ctx context.Context, hostUUID string) error
type SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc func(ctx context.Context, hostID uint, hostUUID string) error
type SetOrUpdateHostEmailsFromMdmIdpAccountsFunc func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error
type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error
@@ -781,13 +777,7 @@ type GetMDMAppleProfilesSummaryFunc func(ctx context.Context, teamID *uint) (*fl
type InsertMDMIdPAccountFunc func(ctx context.Context, account *fleet.MDMIdPAccount) error
type AssociateMDMIdPAccountFunc func(ctx context.Context, accountUUID string, deviceUUID string) error
type GetMDMIdPAccountByAccountUUIDFunc func(ctx context.Context, accountUUID string) (*fleet.MDMIdPAccount, error)
type GetMDMIdPAccountByHostUUIDFunc func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error)
type GetMDMIdPAccountByLegacyEnrollRefFunc func(ctx context.Context, ref string) (*fleet.MDMIdPAccount, error)
type GetMDMIdPAccountByUUIDFunc func(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error)
type GetMDMIdPAccountByEmailFunc func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error)
@@ -1871,14 +1861,8 @@ type DataStore struct {
SetOrUpdateMDMDataFunc SetOrUpdateMDMDataFunc
SetOrUpdateMDMDataFuncInvoked bool
SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc
SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFuncInvoked bool
SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUIDFunc SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUIDFunc
SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUIDFuncInvoked bool
SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc
SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFuncInvoked bool
SetOrUpdateHostEmailsFromMdmIdpAccountsFunc SetOrUpdateHostEmailsFromMdmIdpAccountsFunc
SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked bool
SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFunc
SetOrUpdateHostDisksSpaceFuncInvoked bool
@@ -2141,17 +2125,8 @@ type DataStore struct {
InsertMDMIdPAccountFunc InsertMDMIdPAccountFunc
InsertMDMIdPAccountFuncInvoked bool
AssociateMDMIdPAccountFunc AssociateMDMIdPAccountFunc
AssociateMDMIdPAccountFuncInvoked bool
GetMDMIdPAccountByAccountUUIDFunc GetMDMIdPAccountByAccountUUIDFunc
GetMDMIdPAccountByAccountUUIDFuncInvoked bool
GetMDMIdPAccountByHostUUIDFunc GetMDMIdPAccountByHostUUIDFunc
GetMDMIdPAccountByHostUUIDFuncInvoked bool
GetMDMIdPAccountByLegacyEnrollRefFunc GetMDMIdPAccountByLegacyEnrollRefFunc
GetMDMIdPAccountByLegacyEnrollRefFuncInvoked bool
GetMDMIdPAccountByUUIDFunc GetMDMIdPAccountByUUIDFunc
GetMDMIdPAccountByUUIDFuncInvoked bool
GetMDMIdPAccountByEmailFunc GetMDMIdPAccountByEmailFunc
GetMDMIdPAccountByEmailFuncInvoked bool
@@ -4505,25 +4480,11 @@ func (s *DataStore) SetOrUpdateMDMData(ctx context.Context, hostID uint, isServe
return s.SetOrUpdateMDMDataFunc(ctx, hostID, isServer, enrolled, serverURL, installedFromDep, name, fleetEnrollRef)
}
func (s *DataStore) SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRef(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
func (s *DataStore) SetOrUpdateHostEmailsFromMdmIdpAccounts(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
s.mu.Lock()
s.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFuncInvoked = true
s.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked = true
s.mu.Unlock()
return s.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc(ctx, hostID, fleetEnrollmentRef)
}
func (s *DataStore) SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUID(ctx context.Context, hostUUID string) error {
s.mu.Lock()
s.SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUIDFuncInvoked = true
s.mu.Unlock()
return s.SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUIDFunc(ctx, hostUUID)
}
func (s *DataStore) SetOrUpdateEmailsFromMDMIdPAccountsByHostID(ctx context.Context, hostID uint, hostUUID string) error {
s.mu.Lock()
s.SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFuncInvoked = true
s.mu.Unlock()
return s.SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc(ctx, hostID, hostUUID)
return s.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc(ctx, hostID, fleetEnrollmentRef)
}
func (s *DataStore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error {
@@ -5135,32 +5096,11 @@ func (s *DataStore) InsertMDMIdPAccount(ctx context.Context, account *fleet.MDMI
return s.InsertMDMIdPAccountFunc(ctx, account)
}
func (s *DataStore) AssociateMDMIdPAccount(ctx context.Context, accountUUID string, deviceUUID string) error {
func (s *DataStore) GetMDMIdPAccountByUUID(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
s.mu.Lock()
s.AssociateMDMIdPAccountFuncInvoked = true
s.GetMDMIdPAccountByUUIDFuncInvoked = true
s.mu.Unlock()
return s.AssociateMDMIdPAccountFunc(ctx, accountUUID, deviceUUID)
}
func (s *DataStore) GetMDMIdPAccountByAccountUUID(ctx context.Context, accountUUID string) (*fleet.MDMIdPAccount, error) {
s.mu.Lock()
s.GetMDMIdPAccountByAccountUUIDFuncInvoked = true
s.mu.Unlock()
return s.GetMDMIdPAccountByAccountUUIDFunc(ctx, accountUUID)
}
func (s *DataStore) GetMDMIdPAccountByHostUUID(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
s.mu.Lock()
s.GetMDMIdPAccountByHostUUIDFuncInvoked = true
s.mu.Unlock()
return s.GetMDMIdPAccountByHostUUIDFunc(ctx, hostUUID)
}
func (s *DataStore) GetMDMIdPAccountByLegacyEnrollRef(ctx context.Context, ref string) (*fleet.MDMIdPAccount, error) {
s.mu.Lock()
s.GetMDMIdPAccountByLegacyEnrollRefFuncInvoked = true
s.mu.Unlock()
return s.GetMDMIdPAccountByLegacyEnrollRefFunc(ctx, ref)
return s.GetMDMIdPAccountByUUIDFunc(ctx, uuid)
}
func (s *DataStore) GetMDMIdPAccountByEmail(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
+6 -51
View File
@@ -24,7 +24,6 @@ import (
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
@@ -1286,17 +1285,8 @@ func (svc *Service) EnqueueMDMAppleCommand(
}
type mdmAppleEnrollRequest struct {
// Token is expected to be a UUID string that identifies a template MDM Apple enrollment profile.
Token string `query:"token"`
// EnrollmentReference is expected to be a UUID string that identifies the MDM IdP account used
// to authenticate the end user as part of the MDM IdP flow.
Token string `query:"token"`
EnrollmentReference string `query:"enrollment_reference,optional"`
// DEPDeviceInfo is expected to be a base64 encoded string containing DEP deviceinfo extracted
// from the x-apple-aspen-deviceinfo header of the original configuration web view request and
// persisted by the client in local storage for inclusion in a subsequent enrollment request as
// part of the MDM IdP flow.
// See https://developer.apple.com/documentation/devicemanagement/device_assignment/authenticating_through_web_views
DEPDeviceInfo string `query:"dep_device_info,optional"`
}
func (r mdmAppleEnrollResponse) error() error { return r.Err }
@@ -1326,7 +1316,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, req.EnrollmentReference, req.DEPDeviceInfo)
profile, err := svc.GetMDMAppleEnrollmentProfileByToken(ctx, req.Token, req.EnrollmentReference)
if err != nil {
return mdmAppleEnrollResponse{Err: err}, nil
}
@@ -1335,7 +1325,7 @@ func mdmAppleEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.
}, nil
}
func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, token string, ref string, deviceinfo 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)
@@ -1352,44 +1342,9 @@ func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, tok
return nil, ctxerr.Wrap(ctx, err)
}
enrollURL := appConfig.ServerSettings.ServerURL
// check if there is a legacy enroll ref and preserve it for backwards compatibility
if ref != "" {
idpAcct, err := svc.ds.GetMDMIdPAccountByAccountUUID(
// use the primary db as the account might have been just inserted
ctxdb.RequirePrimary(ctx, true),
ref,
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting MDM IdP account")
}
if idpAcct.FleetEnrollRef != "" {
level.Debug(svc.logger).Log("msg", "using legacy enroll ref", "enroll_ref", idpAcct.FleetEnrollRef)
// we have a legacy enroll ref, add it to the enroll URL
enrollURL, err = apple_mdm.AddEnrollmentRefToFleetURL(appConfig.ServerSettings.ServerURL, idpAcct.FleetEnrollRef)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "adding reference to fleet URL")
}
}
}
// if we have device info, we need to parse it and associate the device with an MDM IdP account
if deviceinfo != "" {
di, err := apple_mdm.ParseDeviceinfo(deviceinfo, true)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "parsing deviceinfo")
}
// try to associate the device with an MDM IdP account
if ref == "" {
// this is unexpected, there should be a reference if we have device info
level.Debug(svc.logger).Log("msg", "associating mdm idp account, missing idp account uuid", "device_uuid", di.UDID)
} else {
// associate the device with the DEP account
if err := svc.ds.AssociateMDMIdPAccount(ctx, ref, di.UDID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "associating MDM IdP account")
}
}
enrollURL, err := apple_mdm.AddEnrollmentRefToFleetURL(appConfig.ServerSettings.ServerURL, ref)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "adding reference to fleet URL")
}
topic, err := svc.mdmPushCertTopic(ctx)
+1 -1
View File
@@ -274,7 +274,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)
-39
View File
@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"github.com/fleetdm/fleet/v4/server/config"
@@ -1038,44 +1037,6 @@ func RedirectSetupToLogin(svc fleet.Service, logger kitlog.Logger, next http.Han
}
}
func WithDEPWebviewRedirect(svc fleet.Service, logger kitlog.Logger, next http.Handler, urlPrefix string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/mdm/sso" && r.URL.RawQuery == "" {
level.Debug(logger).Log("msg", "handling mdm sso", "url", r.URL.String())
// Note: We'll apply this redirect only if query params are empty because want to
// redirect to the same URL with added query params after parsing the x-apple-aspen-deviceinfo
// header. Whenever we see a request with any query params already present, we'll
// skip this step and just continue to the next handler.
di := r.Header.Get("X-apple-aspen-deviceinfo")
if di != "" {
level.Debug(logger).Log("msg", "parsing X-apple-aspen-deviceinfo", "url", r.URL.String())
// extract x-apple-aspen-deviceinfo custom header from request
_, err := apple_mdm.ParseDeviceinfo(di, true)
if err != nil {
level.Error(logger).Log("msg", "parsing X-apple-aspen-deviceinfo", "err", err)
http.Redirect(w, r, r.URL.String()+"?error=true", http.StatusSeeOther)
return
}
// redirect to the same URL with added deviceinfo query params
newURL := r.URL
q := url.Values{
"dep_device_info": []string{di},
}
newURL.RawQuery = q.Encode()
level.Debug(logger).Log("msg", "adding query params to redirect url", "query", newURL.RawQuery)
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
return
}
// TODO: consider whether we want always return an error here if the header is missing for this endpoint?
level.Info(logger).Log("msg", "missig x-apple-aspen-deviceinfo header, continuing to next")
}
next.ServeHTTP(w, r)
return
}
}
// RegisterAppleMDMProtocolServices registers the HTTP handlers that serve
// the MDM services to Apple devices.
func RegisterAppleMDMProtocolServices(
+5 -17
View File
@@ -327,6 +327,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
require.NoError(s.T(), json.NewEncoder(w).Encode(s.mockedDownloadFleetdmMeta))
}
}))
s.T().Setenv("FLEET_DEV_DOWNLOAD_FLEETDM_URL", downloadFleetdmSrv.URL)
@@ -4593,7 +4594,7 @@ func (s *integrationMDMTestSuite) TestSSO() {
require.Equal(t, lastSubmittedProfile.ConfigurationWebURL, lastSubmittedProfile.URL)
checkStoredIdPInfo := func(uuid, username, fullname, email string) {
acc, err := s.ds.GetMDMIdPAccountByAccountUUID(context.Background(), uuid)
acc, err := s.ds.GetMDMIdPAccountByUUID(context.Background(), uuid)
require.NoError(t, err)
require.Equal(t, username, acc.Username)
require.Equal(t, fullname, acc.Fullname)
@@ -4722,11 +4723,6 @@ func (s *integrationMDMTestSuite) TestSSO() {
// 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)
// associate the mdm idp account with the host uuid manually (we can't mock the Apple webview client
// deviceinfo header because it is signed with the Apple Root CA)
require.NoError(t, s.ds.AssociateMDMIdPAccount(context.Background(), user1EnrollRef, mdmDevice.UUID))
err = mdmDevice.Enroll()
require.NoError(t, err)
@@ -4754,22 +4750,13 @@ func (s *integrationMDMTestSuite) TestSSO() {
require.Equal(t, "SSO User 1", fullAccCmd.Command.AccountConfiguration.PrimaryAccountFullName)
require.Equal(t, "sso_user", fullAccCmd.Command.AccountConfiguration.PrimaryAccountUserName)
// check that the host was created and get the host id and other details for the next steps
// report host details for the device
var hostResp getHostResponse
s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
// ensure that host_emails entry was created from mdm_idp_accounts
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
var email string
err := sqlx.GetContext(context.Background(), q, &email, `SELECT email FROM host_emails WHERE host_id = ? AND source = ?`, hostResp.Host.ID, fleet.DeviceMappingMDMIdpAccounts)
require.NoError(t, err)
require.Equal(t, "sso_user@example.com", email)
return nil
})
// get app config and host detail queries
ac, err := s.ds.AppConfig(context.Background())
require.NoError(t, err)
detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features)
// simulate osquery reporting mdm information
@@ -8306,6 +8293,7 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
// lock the host without viewing the PIN
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusNoContent)
}
func (s *integrationMDMTestSuite) TestZCustomConfigurationWebURL() {
+5 -12
View File
@@ -1712,18 +1712,11 @@ func directIngestMDMMac(ctx context.Context, logger log.Logger, host *fleet.Host
fleetEnrollRef = serverURL.Query().Get("enrollment_reference")
}
if fleetEnrollRef != "" {
if err := ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRef(ctx, host.ID, fleetEnrollRef); err != nil {
level.Warn(logger).Log(
"component", "service",
"method", "directIngestMDMMac",
"msg", err.Error(),
)
}
} else if installedFromDep {
// ensure that mdm_idp_accounts are included in host_emails for Apple DEP-enrolled hosts;
// note that we're relying on the osquery platform compatibility checks to ensure that this
// ingest function is only applied darwin hosts
if err := ds.SetOrUpdateEmailsFromMDMIdPAccountsByHostID(ctx, host.ID, host.UUID); err != nil {
if err := ds.SetOrUpdateHostEmailsFromMdmIdpAccounts(ctx, host.ID, fleetEnrollRef); err != nil {
if !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "updating host emails from mdm idp accounts")
}
level.Warn(logger).Log(
"component", "service",
"method", "directIngestMDMMac",
+8 -17
View File
@@ -596,15 +596,12 @@ func TestDirectIngestMDMMac(t *testing.T) {
require.Equal(t, fleetEnrollmentRef, c.enrollRef)
return nil
}
ds.SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc = func(ctx context.Context, hostID uint, hostUUID string) error {
return nil
}
ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
return nil
}
if c.name == "with invalid enrollment reference" {
ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
return &nfe{}
}
}
@@ -619,7 +616,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
require.NoError(t, err)
ds.SetOrUpdateMDMDataFuncInvoked = false
if c.name != "with invalid enrollment reference" {
require.False(t, ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFuncInvoked)
require.False(t, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
}
}
})
@@ -687,13 +684,10 @@ func TestDirectIngestMDMFleetEnrollRef(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
require.Equal(t, tc.wantEnrollRef, fleetEnrollmentRef)
return nil
}
ds.SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc = func(ctx context.Context, hostID uint, hostUUID string) error {
return nil
}
ds.SetOrUpdateMDMDataFunc = func(ctx context.Context, hostID uint, isServer, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollmentRef string) error {
require.False(t, isServer)
require.True(t, enrolled)
@@ -717,8 +711,8 @@ func TestDirectIngestMDMFleetEnrollRef(t *testing.T) {
require.NoError(t, err)
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
ds.SetOrUpdateMDMDataFuncInvoked = false
require.Equal(t, tc.wantHostEmailsCalled, ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFuncInvoked)
ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFuncInvoked = false
require.Equal(t, tc.wantHostEmailsCalled, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked = false
})
}
}
@@ -942,10 +936,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
require.Empty(t, fleetEnrollmentRef)
return nil
}
ds.SetOrUpdateEmailsFromMDMIdPAccountsByHostIDFunc = func(ctx context.Context, hostID uint, hostUUID string) error {
return nil
}
ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
return nil
}
})
@@ -953,7 +944,7 @@ func TestDirectIngestMDMWindows(t *testing.T) {
require.NoError(t, err)
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
ds.SetOrUpdateMDMDataFuncInvoked = false
require.False(t, ds.SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRefFuncInvoked)
require.False(t, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
}
}
+8 -25
View File
@@ -9,7 +9,6 @@ import (
"time"
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
@@ -107,9 +106,6 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg
func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error {
var awaitCmdUUIDs []string
// use primary db to ensure we have the latest records for enrollments
ctx = ctxdb.RequirePrimary(ctx, true)
if isMacOS(args.Platform) {
fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID)
if err != nil {
@@ -126,24 +122,18 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
}
}
idpAcct, err := a.Datastore.GetMDMIdPAccountByHostUUID(ctx, args.HostUUID)
if err != nil && !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "getting idp account details")
}
if ref := args.EnrollReference; ref != "" {
a.Log.Log("info", "got an enroll_reference", "host_uuid", args.HostUUID, "ref", ref)
idpAcct, err = a.Datastore.GetMDMIdPAccountByLegacyEnrollRef(ctx, ref)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting idp account details for enroll reference %s", ref)
}
}
if idpAcct != nil {
appCfg, err := a.Datastore.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting app config")
}
acct, err := a.Datastore.GetMDMIdPAccountByUUID(ctx, ref)
if err != nil {
return ctxerr.Wrapf(ctx, err, "getting idp account details for enroll reference %s", ref)
}
ssoEnabled := appCfg.MDM.MacOSSetup.EnableEndUserAuthentication
if args.TeamID != nil {
team, err := a.Datastore.Team(ctx, *args.TeamID)
@@ -160,19 +150,12 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
ctx,
[]string{args.HostUUID},
cmdUUID,
idpAcct.Fullname,
idpAcct.Username,
acct.Fullname,
acct.Username,
); err != nil {
return ctxerr.Wrap(ctx, err, "sending AccountConfiguration command")
}
awaitCmdUUIDs = append(awaitCmdUUIDs, cmdUUID)
// NOTE: We only set the email address here if we have an MDM IdP account and sso is enabled. We rely on the
// `resetDarwin` lifecycle event to delete from `host_emails` for this host uuid if there is
// any email where `source = 'mdm_idp_account'`.
if err := a.Datastore.SetOrUpdateHostEmailsFromMDMIdPAccountsByHostUUID(ctx, args.HostUUID); err != nil {
return ctxerr.Wrap(ctx, err, "setting host emails from mdm idp accounts")
}
}
}
+4 -160
View File
@@ -2,7 +2,6 @@ package worker
import (
"context"
"database/sql"
"fmt"
"os"
"testing"
@@ -403,7 +402,7 @@ func TestAppleMDM(t *testing.T) {
require.Equal(t, "custom-team-bootstrap", ms.BootstrapPackageName)
})
t.Run("unknown legacy enroll reference", func(t *testing.T) {
t.Run("unknown enroll reference", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds)
defer mysql.TruncateTables(t, ds)
@@ -431,12 +430,12 @@ func TestAppleMDM(t *testing.T) {
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Time{})
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Contains(t, jobs[0].Error, "MDMIdPAccount with fleet_enroll_ref abcd was not found")
require.Contains(t, jobs[0].Error, "MDMIdPAccount with uuid abcd was not found")
require.Equal(t, fleet.JobStateQueued, jobs[0].State)
require.Equal(t, 1, jobs[0].Retries)
})
t.Run("legacy reference associated MDM IdP account but SSO disabled", func(t *testing.T) {
t.Run("enroll reference but SSO disabled", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds)
defer mysql.TruncateTables(t, ds)
@@ -450,15 +449,8 @@ func TestAppleMDM(t *testing.T) {
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
require.NoError(t, err)
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE mdm_idp_accounts mia SET fleet_enroll_ref = mia.uuid WHERE uuid = ?`, idpAcc.UUID)
return err
})
h := createEnrolledHost(t, 1, nil, true)
require.NoError(t, ds.AssociateMDMIdPAccount(ctx, idpAcc.UUID, h.UUID))
mdmWorker := &AppleMDM{
Datastore: ds,
Log: nopLog,
@@ -489,17 +481,9 @@ func TestAppleMDM(t *testing.T) {
// confirm that AccountConfiguration command was not enqueued
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
// ensure that host_emails entry was not created from mdm_idp_accounts
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var gotEmail string
err := sqlx.GetContext(ctx, q, &gotEmail, `SELECT email FROM host_emails WHERE host_id = ? AND source = ?`, h.ID, fleet.DeviceMappingMDMIdpAccounts)
require.ErrorIs(t, err, sql.ErrNoRows)
return nil
})
})
t.Run("legacy reference associated MDM IdP account with SSO enabled", func(t *testing.T) {
t.Run("enroll reference with SSO enabled", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds)
defer mysql.TruncateTables(t, ds)
@@ -513,11 +497,6 @@ func TestAppleMDM(t *testing.T) {
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
require.NoError(t, err)
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE mdm_idp_accounts mia SET fleet_enroll_ref = mia.uuid WHERE uuid = ?`, idpAcc.UUID)
return err
})
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
require.NoError(t, err)
tm, err = ds.Team(ctx, tm.ID)
@@ -528,8 +507,6 @@ func TestAppleMDM(t *testing.T) {
h := createEnrolledHost(t, 1, &tm.ID, true)
require.NoError(t, ds.AssociateMDMIdPAccount(ctx, idpAcc.UUID, h.UUID))
mdmWorker := &AppleMDM{
Datastore: ds,
Log: nopLog,
@@ -559,139 +536,6 @@ func TestAppleMDM(t *testing.T) {
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "AccountConfiguration"}, getEnqueuedCommandTypes(t))
// ensure that the host_emails entry was created from mdm_idp_accounts
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var gotEmail string
err := sqlx.GetContext(ctx, q, &gotEmail, `SELECT email FROM host_emails WHERE host_id = ? AND source = ?`, h.ID, fleet.DeviceMappingMDMIdpAccounts)
require.NoError(t, err)
require.Equal(t, "test@example.com", gotEmail)
return nil
})
})
t.Run("associated MDM IdP account with SSO enabled", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds)
defer mysql.TruncateTables(t, ds)
err := ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{
Username: "test",
Fullname: "test",
Email: "test@example.com",
})
require.NoError(t, err)
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
require.NoError(t, err)
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
require.NoError(t, err)
tm, err = ds.Team(ctx, tm.ID)
require.NoError(t, err)
tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true
_, err = ds.SaveTeam(ctx, tm)
require.NoError(t, err)
h := createEnrolledHost(t, 1, &tm.ID, true)
require.NoError(t, ds.AssociateMDMIdPAccount(ctx, idpAcc.UUID, h.UUID))
mdmWorker := &AppleMDM{
Datastore: ds,
Log: nopLog,
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
}
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
require.NoError(t, err)
// run the worker, should succeed
err = w.ProcessJobs(ctx)
require.NoError(t, err)
// ensure the job's not_before allows it to be returned if it were to run
// again
time.Sleep(time.Second)
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
require.NoError(t, err)
// the post-DEP release device job is pending
require.Len(t, jobs, 1)
require.Equal(t, appleMDMJobName, jobs[0].Name)
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "AccountConfiguration"}, getEnqueuedCommandTypes(t))
// ensure that the host_emails entry was created from mdm_idp_accounts
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var gotEmail string
err := sqlx.GetContext(ctx, q, &gotEmail, `SELECT email FROM host_emails WHERE host_id = ? AND source = ?`, h.ID, fleet.DeviceMappingMDMIdpAccounts)
require.NoError(t, err)
require.Equal(t, "test@example.com", gotEmail)
return nil
})
})
t.Run("associated MDM IdP account but SSO disabled", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds)
defer mysql.TruncateTables(t, ds)
err := ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{
Username: "test",
Fullname: "test",
Email: "test@example.com",
})
require.NoError(t, err)
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
require.NoError(t, err)
h := createEnrolledHost(t, 1, nil, true)
require.NoError(t, ds.AssociateMDMIdPAccount(ctx, idpAcc.UUID, h.UUID))
mdmWorker := &AppleMDM{
Datastore: ds,
Log: nopLog,
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
}
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
require.NoError(t, err)
// run the worker, should succeed
err = w.ProcessJobs(ctx)
require.NoError(t, err)
// ensure the job's not_before allows it to be returned if it were to run
// again
time.Sleep(time.Second)
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
require.NoError(t, err)
// the post-DEP release device job is pending, having failed its first attempt
require.Len(t, jobs, 1)
require.Equal(t, appleMDMJobName, jobs[0].Name)
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
require.Equal(t, 0, jobs[0].Retries) // hasn't run yet
// confirm that AccountConfiguration command was not enqueued
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
// ensure that host_emails entry was not created from mdm_idp_accounts
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var gotEmail string
err := sqlx.GetContext(ctx, q, &gotEmail, `SELECT email FROM host_emails WHERE host_id = ? AND source = ?`, h.ID, fleet.DeviceMappingMDMIdpAccounts)
require.ErrorIs(t, err, sql.ErrNoRows)
return nil
})
})
t.Run("installs fleetd for manual enrollments", func(t *testing.T) {