mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
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:
parent
b0828e8b88
commit
8e532a5e76
1
changes/10744-dep-acct-creation
Normal file
1
changes/10744-dep-acct-creation
Normal file
@ -0,0 +1 @@
|
||||
* MDM: DEP enrollments configured with SSO now pre-populate the username/fullname fields during account creation.
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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`,
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
@ -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
@ -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).
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
//
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user