Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve device user account creation during MDM IdP enrollment flow #20162

Merged
merged 16 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changes/19185-dep-deviceinfo-mdm-idp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- 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 changes: 2 additions & 0 deletions cmd/fleet/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,8 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const EnrollmentGate = ({
}: IEnrollmentGateProps) => {
const [showEULA, setShowEULA] = useState(Boolean(eulaToken));

const dep_device_info = localStorage.getItem("dep_device_info") || "";

if (!profileToken || error) {
return <SSOError />;
}
Expand All @@ -55,10 +57,11 @@ const EnrollmentGate = ({

return (
<RedirectTo
url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE(
profileToken,
enrollmentReference
)}
url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE({
token: profileToken,
ref: enrollmentReference,
gillespi314 marked this conversation as resolved.
Show resolved Hide resolved
dep_device_info,
})}
/>
);
};
Expand Down
14 changes: 10 additions & 4 deletions frontend/pages/MDMAppleSSOPage/MDMAppleSSOPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import React from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
import { WithRouterProps } from "react-router";

import mdmAPI from "services/entities/mdm";
import mdmAPI, { IMDMSSOParams } from "services/entities/mdm";

import SSOError from "components/MDM/SSOError";
import Spinner from "components/Spinner/Spinner";
import { IMdmSSOReponse } from "interfaces/mdm";

const baseClass = "mdm-apple-sso-page";

const DEPSSOLoginPage = () => {
const { error } = useQuery<void, AxiosError, IMdmSSOReponse>(
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>(
["dep_sso"],
() => mdmAPI.initiateMDMAppleSSO(),
() => mdmAPI.initiateMDMAppleSSO(query),
{
retry: false,
refetchOnWindowFocus: false,
Expand Down
15 changes: 13 additions & 2 deletions frontend/services/entities/mdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DiskEncryptionStatus,
IHostMdmProfile,
IMdmProfile,
IMdmSSOReponse,
MdmProfileStatus,
} from "interfaces/mdm";
import { API_NO_TEAM_ID } from "interfaces/team";
Expand Down Expand Up @@ -67,6 +68,16 @@ 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;
Expand Down Expand Up @@ -181,9 +192,9 @@ const mdmService = {
});
},

initiateMDMAppleSSO: () => {
initiateMDMAppleSSO: (params: IMDMSSOParams): Promise<IMdmSSOReponse> => {
const { MDM_APPLE_SSO } = endpoints;
return sendRequest("POST", MDM_APPLE_SSO, {});
return sendRequest("POST", MDM_APPLE_SSO, params);
},

getBootstrapPackageMetadata: (teamId: number) => {
Expand Down
13 changes: 9 additions & 4 deletions frontend/utilities/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IMDMAppleEnrollmentProfileParams } from "services/entities/mdm";

const API_VERSION = "latest";

export default {
Expand Down Expand Up @@ -93,11 +95,14 @@ 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: string, ref?: string) => {
MDM_APPLE_ENROLLMENT_PROFILE: ({
token,
ref,
dep_device_info,
}: IMDMAppleEnrollmentProfileParams) => {
const query = new URLSearchParams({ token });
if (ref) {
query.append("enrollment_reference", ref);
}
ref && query.append("enrollment_reference", ref);
dep_device_info && query.append("dep_device_info", dep_device_info);
return `/api/mdm/apple/enroll?${query}`;
},
MDM_APPLE_SETUP_ENROLLMENT_PROFILE: `/${API_VERSION}/fleet/mdm/apple/enrollment_profile`,
Expand Down
55 changes: 50 additions & 5 deletions server/datastore/mysql/apple_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2725,8 +2725,13 @@ 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 FROM mdm_idp_accounts WHERE email = ?`
stmt := `SELECT uuid, username, fullname, email, host_uuid, fleet_enroll_ref FROM mdm_idp_accounts WHERE email = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, email)
if err != nil {
Expand All @@ -2738,13 +2743,39 @@ func (ds *Datastore) GetMDMIdPAccountByEmail(ctx context.Context, email string)
return &acct, nil
}

func (ds *Datastore) GetMDMIdPAccountByUUID(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
stmt := `SELECT uuid, username, fullname, email FROM mdm_idp_accounts WHERE uuid = ?`
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 = ?`
var acct fleet.MDMIdPAccount
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, uuid)
err := sqlx.GetContext(ctx, ds.reader(ctx), &acct, stmt, accountUUID)
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, 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, err, "select mdm_idp_accounts")
}
Expand Down Expand Up @@ -3638,6 +3669,20 @@ 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
Expand Down
4 changes: 2 additions & 2 deletions server/datastore/mysql/apple_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2469,11 +2469,11 @@ func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) {
require.ErrorAs(t, err, &nfe)
require.Nil(t, out)

out, err = ds.GetMDMIdPAccountByUUID(ctx, acc.UUID)
out, err = ds.GetMDMIdPAccountByAccountUUID(ctx, acc.UUID)
require.NoError(t, err)
require.Equal(t, acc, out)

out, err = ds.GetMDMIdPAccountByUUID(ctx, "BAD-TOKEN")
out, err = ds.GetMDMIdPAccountByAccountUUID(ctx, "BAD-TOKEN")
require.ErrorAs(t, err, &nfe)
require.Nil(t, out)
}
Expand Down
77 changes: 68 additions & 9 deletions server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3629,19 +3629,78 @@ func (ds *Datastore) SetOrUpdateMDMData(
)
}

func (ds *Datastore) SetOrUpdateHostEmailsFromMdmIdpAccounts(
func (ds *Datastore) SetOrUpdateHostEmailsFromMDMIdPAccountsByLegacyEnrollRef(
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
if fleetEnrollmentRef != "" {
idp, err := ds.GetMDMIdPAccountByUUID(ctx, fleetEnrollmentRef)
if err != nil {
return err
}
email = &idp.Email
idp, err := ds.GetMDMIdPAccountByLegacyEnrollRef(ctx, fleetEnrollmentRef)
if err != nil {
return err
}
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,
Expand Down Expand Up @@ -5135,8 +5194,8 @@ func (ds *Datastore) loadHostLite(ctx context.Context, id *uint, identifier *str
SELECT
h.id,
h.team_id,
h.osquery_host_id,
h.node_key,
COALESCE(h.osquery_host_id, '') AS osquery_host_id,
COALESCE(h.node_key, '') AS node_key,
gillespi314 marked this conversation as resolved.
Show resolved Hide resolved
h.hostname,
h.uuid,
h.hardware_serial,
Expand Down
1 change: 1 addition & 0 deletions server/datastore/mysql/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7623,6 +7623,7 @@ 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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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
}
Loading
Loading