mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34433 It speeds up the cron, meaning fleetd, bootstrap and now profiles should be sent within 10 seconds of being known to fleet, compared to the previous 1 minute. It's heavily based on my last PR, so the structure and changes are close to identical, with some small differences. **I did not do the redis key part in this PR, as I think that should come in it's own PR, to avoid overlooking logic bugs with that code, and since this one is already quite sized since we're moving core pieces of code around.** # 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/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Faster macOS onboarding: device profiles are delivered and installed as part of DEP enrollment, shortening initial setup. * Improved profile handling: per-host profile preprocessing, secret detection, and clearer failure marking. * **Improvements** * Consolidated SCEP/NDES error messaging for clearer diagnostics. * Cron/work scheduling tuned to prioritize Apple MDM profile delivery. * **Tests** * Expanded MDM unit and integration tests, including DeclarativeManagement handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
5970 lines
210 KiB
Go
5970 lines
210 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
_ "embed"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
|
"github.com/fleetdm/fleet/v4/server/dev_mode"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
fleetmdm "github.com/fleetdm/fleet/v4/server/mdm"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
|
mdmlifecycle "github.com/fleetdm/fleet/v4/server/mdm/lifecycle"
|
|
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
|
nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
|
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
|
"github.com/micromdm/nanolib/log/stdlogfmt"
|
|
"github.com/micromdm/plist"
|
|
"github.com/smallstep/pkcs7"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
|
|
)
|
|
|
|
type nopProfileMatcher struct{}
|
|
|
|
func (nopProfileMatcher) PreassignProfile(ctx context.Context, pld fleet.MDMApplePreassignProfilePayload) error {
|
|
return nil
|
|
}
|
|
|
|
func (nopProfileMatcher) RetrieveProfiles(ctx context.Context, extHostID string) (fleet.MDMApplePreassignHostProfiles, error) {
|
|
return fleet.MDMApplePreassignHostProfiles{}, nil
|
|
}
|
|
|
|
func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Service, context.Context, *mock.Store, *TestServerOpts) {
|
|
ds := new(mock.Store)
|
|
cfg := config.TestConfig()
|
|
testCertPEM, testKeyPEM, err := generateCertWithAPNsTopic()
|
|
require.NoError(t, err)
|
|
config.SetTestMDMConfig(t, &cfg, testCertPEM, testKeyPEM, "../../server/service/testdata")
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/server/devices"):
|
|
_, err := w.Write([]byte("{}"))
|
|
require.NoError(t, err)
|
|
return
|
|
case strings.Contains(r.URL.Path, "/session"):
|
|
_, err := w.Write([]byte(`{"auth_session_token": "yoo"}`))
|
|
require.NoError(t, err)
|
|
return
|
|
case strings.Contains(r.URL.Path, "/profile"):
|
|
_, err := w.Write([]byte(`{"profile_uuid": "profile123"}`))
|
|
require.NoError(t, err)
|
|
}
|
|
}))
|
|
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
depStorage := &nanodep_mock.Storage{}
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))),
|
|
)
|
|
|
|
opts := &TestServerOpts{
|
|
FleetConfig: &cfg,
|
|
MDMStorage: mdmStorage,
|
|
DEPStorage: depStorage,
|
|
MDMPusher: pusher,
|
|
License: license,
|
|
ProfileMatcher: nopProfileMatcher{},
|
|
}
|
|
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, opts)
|
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
|
|
return nil, nil
|
|
}
|
|
mdmStorage.RetrievePushInfoFunc = func(ctx context.Context, tokens []string) (map[string]*mdm.Push, error) {
|
|
res := make(map[string]*mdm.Push, len(tokens))
|
|
for _, t := range tokens {
|
|
res[t] = &mdm.Push{
|
|
PushMagic: "",
|
|
Token: []byte(t),
|
|
Topic: "",
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
mdmStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
|
|
cert, err := tls.LoadX509KeyPair("testdata/server.pem", "testdata/server.key")
|
|
return &cert, "", err
|
|
}
|
|
mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
|
|
return &nanodep_client.OAuth1Tokens{}, nil
|
|
}
|
|
depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) {
|
|
return &nanodep_client.Config{
|
|
BaseURL: ts.URL,
|
|
}, nil
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
OrgInfo: fleet.OrgInfo{
|
|
OrgName: "Foo Inc.",
|
|
},
|
|
ServerSettings: fleet.ServerSettings{
|
|
ServerURL: "https://foo.example.com",
|
|
},
|
|
MDM: fleet.MDM{
|
|
EnabledAndConfigured: true,
|
|
},
|
|
}, nil
|
|
}
|
|
ds.GetMDMAppleEnrollmentProfileByTokenFunc = func(ctx context.Context, token string) (*fleet.MDMAppleEnrollmentProfile, error) {
|
|
return nil, nil
|
|
}
|
|
ds.NewMDMAppleEnrollmentProfileFunc = func(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) {
|
|
return &fleet.MDMAppleEnrollmentProfile{
|
|
ID: 1,
|
|
Token: "foo",
|
|
Type: fleet.MDMAppleEnrollmentTypeManual,
|
|
EnrollmentURL: "https://foo.example.com?token=foo",
|
|
}, nil
|
|
}
|
|
ds.GetMDMAppleEnrollmentProfileByTokenFunc = func(ctx context.Context, token string) (*fleet.MDMAppleEnrollmentProfile, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListMDMAppleEnrollmentProfilesFunc = func(ctx context.Context) ([]*fleet.MDMAppleEnrollmentProfile, error) {
|
|
return nil, nil
|
|
}
|
|
ds.NewMDMAppleInstallerFunc = func(ctx context.Context, name string, size int64, manifest string, installer []byte, urlToken string) (*fleet.MDMAppleInstaller, error) {
|
|
return nil, nil
|
|
}
|
|
ds.MDMAppleInstallerFunc = func(ctx context.Context, token string) (*fleet.MDMAppleInstaller, error) {
|
|
return nil, nil
|
|
}
|
|
ds.MDMAppleInstallerDetailsByIDFunc = func(ctx context.Context, id uint) (*fleet.MDMAppleInstaller, error) {
|
|
return nil, nil
|
|
}
|
|
ds.DeleteMDMAppleInstallerFunc = func(ctx context.Context, id uint) error {
|
|
return nil
|
|
}
|
|
ds.MDMAppleInstallerDetailsByTokenFunc = func(ctx context.Context, token string) (*fleet.MDMAppleInstaller, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListMDMAppleInstallersFunc = func(ctx context.Context) ([]fleet.MDMAppleInstaller, error) {
|
|
return nil, nil
|
|
}
|
|
ds.MDMAppleListDevicesFunc = func(ctx context.Context) ([]fleet.MDMAppleDevice, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
return &fleet.NanoEnrollment{Enabled: false}, nil
|
|
}
|
|
ds.GetNanoMDMEnrollmentTimesFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, error) {
|
|
return nil, nil, nil
|
|
}
|
|
ds.GetMDMAppleCommandRequestTypeFunc = func(ctx context.Context, commandUUID string) (string, error) {
|
|
return "", nil
|
|
}
|
|
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
|
|
return &fleet.MDMEULA{}, nil
|
|
}
|
|
ds.MDMGetEULABytesFunc = func(ctx context.Context, token string) (*fleet.MDMEULA, error) {
|
|
return &fleet.MDMEULA{}, nil
|
|
}
|
|
ds.MDMInsertEULAFunc = func(ctx context.Context, eula *fleet.MDMEULA) error {
|
|
return nil
|
|
}
|
|
ds.MDMDeleteEULAFunc = func(ctx context.Context, token string) error {
|
|
return nil
|
|
}
|
|
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
|
|
return nil
|
|
}
|
|
ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) {
|
|
return document, nil
|
|
}
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
|
|
return document, nil, nil
|
|
}
|
|
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
|
require.NoError(t, err)
|
|
crt, key, err := apple_mdm.NewSCEPCACertKey()
|
|
require.NoError(t, err)
|
|
certPEM := tokenpki.PEMCertificate(crt.Raw)
|
|
keyPEM := tokenpki.PEMRSAPrivateKey(key)
|
|
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetAPNSCert: {Value: apnsCert},
|
|
fleet.MDMAssetAPNSKey: {Value: apnsKey},
|
|
fleet.MDMAssetCACert: {Value: certPEM},
|
|
fleet.MDMAssetCAKey: {Value: keyPEM},
|
|
}, nil
|
|
}
|
|
|
|
ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
|
|
return []string{"foobar"}, nil
|
|
}
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{{ID: 1}}, nil
|
|
}
|
|
|
|
return svc, ctx, ds, opts
|
|
}
|
|
|
|
func TestAppleMDMAuthorization(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
|
|
return []*fleet.EnrollSecret{
|
|
{
|
|
Secret: "abcd",
|
|
TeamID: nil,
|
|
},
|
|
{
|
|
Secret: "efgh",
|
|
TeamID: nil,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) {
|
|
return &fleet.EnrollSecret{
|
|
Secret: "abcd",
|
|
TeamID: nil,
|
|
}, nil
|
|
}
|
|
|
|
checkAuthErr := func(t *testing.T, err error, shouldFailWithAuth bool) {
|
|
t.Helper()
|
|
|
|
if shouldFailWithAuth {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
testAuthdMethods := func(t *testing.T, user *fleet.User, shouldFailWithAuth bool) {
|
|
ctx := test.UserContext(ctx, user)
|
|
_, err := svc.UploadMDMAppleInstaller(ctx, "foo", 3, bytes.NewReader([]byte("foo")))
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
_, err = svc.GetMDMAppleInstallerByID(ctx, 42)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
err = svc.DeleteMDMAppleInstaller(ctx, 42)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
_, err = svc.ListMDMAppleInstallers(ctx)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
_, err = svc.ListMDMAppleDevices(ctx)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
}
|
|
|
|
// some eula methods read and write access for gitops users. We test them separately
|
|
// from the other MDM methods.
|
|
testEULAMethods := func(t *testing.T, user *fleet.User, shouldFailWithAuth bool) {
|
|
ctx := test.UserContext(ctx, user)
|
|
_, err := svc.MDMGetEULAMetadata(ctx)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
err = svc.MDMCreateEULA(ctx, "eula.pdf", bytes.NewReader([]byte("%PDF-")), false)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
err = svc.MDMDeleteEULA(ctx, "foo", false)
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
}
|
|
|
|
// Only global admins can access the endpoints.
|
|
testAuthdMethods(t, test.UserAdmin, false)
|
|
|
|
// Global admin and gitops users can access the eula endpoints.
|
|
testEULAMethods(t, test.UserAdmin, false)
|
|
testEULAMethods(t, test.UserGitOps, false)
|
|
|
|
// All other users should not have access to the endpoints.
|
|
for _, user := range []*fleet.User{
|
|
test.UserNoRoles,
|
|
test.UserMaintainer,
|
|
test.UserObserver,
|
|
test.UserObserverPlus,
|
|
test.UserTeamAdminTeam1,
|
|
} {
|
|
testAuthdMethods(t, user, true)
|
|
testEULAMethods(t, user, true)
|
|
}
|
|
// Token authenticated endpoints can be accessed by anyone.
|
|
ctx = test.UserContext(ctx, test.UserNoRoles)
|
|
_, err := svc.GetMDMAppleInstallerByToken(ctx, "foo")
|
|
require.NoError(t, err)
|
|
_, err = svc.GetMDMAppleEnrollmentProfileByToken(ctx, "foo", "")
|
|
require.NoError(t, err)
|
|
_, err = svc.GetMDMAppleInstallerDetailsByToken(ctx, "foo")
|
|
require.NoError(t, err)
|
|
_, err = svc.MDMGetEULABytes(ctx, "foo")
|
|
require.NoError(t, err)
|
|
// Generating a new key pair does not actually make any changes to fleet, or expose any
|
|
// information. The user must configure fleet with the new key pair and restart the server.
|
|
_, err = svc.NewMDMAppleDEPKeyPair(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Should work for all user types
|
|
for _, user := range []*fleet.User{
|
|
test.UserAdmin,
|
|
test.UserMaintainer,
|
|
test.UserObserver,
|
|
test.UserObserverPlus,
|
|
test.UserTeamAdminTeam1,
|
|
test.UserTeamGitOpsTeam1,
|
|
test.UserGitOps,
|
|
test.UserTeamMaintainerTeam1,
|
|
test.UserTeamObserverTeam1,
|
|
test.UserTeamObserverPlusTeam1,
|
|
} {
|
|
usrctx := test.UserContext(ctx, user)
|
|
_, err = svc.GetMDMManualEnrollmentProfile(usrctx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Must be device-authenticated, should fail
|
|
_, err = svc.GetDeviceMDMAppleEnrollmentProfile(ctx)
|
|
checkAuthErr(t, err, true)
|
|
// works with device-authenticated context
|
|
hostCtx := test.HostContext(context.Background(), &fleet.Host{})
|
|
_, err = svc.GetDeviceMDMAppleEnrollmentProfile(hostCtx)
|
|
require.NoError(t, err)
|
|
|
|
hostUUIDsToTeamID := map[string]uint{
|
|
"host1": 1,
|
|
"host2": 1,
|
|
"host3": 2,
|
|
"host4": 0,
|
|
}
|
|
ds.ListHostsLiteByUUIDsFunc = func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) {
|
|
hosts := make([]*fleet.Host, 0, len(uuids))
|
|
for _, uuid := range uuids {
|
|
tmID := hostUUIDsToTeamID[uuid]
|
|
if tmID == 0 {
|
|
hosts = append(hosts, &fleet.Host{UUID: uuid, TeamID: nil})
|
|
} else {
|
|
hosts = append(hosts, &fleet.Host{UUID: uuid, TeamID: &tmID})
|
|
}
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
rawB64FreeCmd := base64.RawStdEncoding.EncodeToString([]byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Command</key>
|
|
<dict>
|
|
<key>RequestType</key>
|
|
<string>FooBar</string>
|
|
</dict>
|
|
<key>CommandUUID</key>
|
|
<string>uuid</string>
|
|
</dict>
|
|
</plist>`))
|
|
|
|
t.Run("EnqueueMDMAppleCommand", func(t *testing.T) {
|
|
enqueueCmdCases := []struct {
|
|
desc string
|
|
user *fleet.User
|
|
uuids []string
|
|
shoudFailWithAuth bool
|
|
}{
|
|
{"no role", test.UserNoRoles, []string{"host1", "host2", "host3", "host4"}, true},
|
|
{"maintainer can run", test.UserMaintainer, []string{"host1", "host2", "host3", "host4"}, false},
|
|
{"admin can run", test.UserAdmin, []string{"host1", "host2", "host3", "host4"}, false},
|
|
{"observer cannot run", test.UserObserver, []string{"host1", "host2", "host3", "host4"}, true},
|
|
{"team 1 admin can run team 1", test.UserTeamAdminTeam1, []string{"host1", "host2"}, false},
|
|
{"team 2 admin can run team 2", test.UserTeamAdminTeam2, []string{"host3"}, false},
|
|
{"team 1 maintainer can run team 1", test.UserTeamMaintainerTeam1, []string{"host1", "host2"}, false},
|
|
{"team 1 observer cannot run team 1", test.UserTeamObserverTeam1, []string{"host1", "host2"}, true},
|
|
{"team 1 admin cannot run team 2", test.UserTeamAdminTeam1, []string{"host3"}, true},
|
|
{"team 1 admin cannot run no team", test.UserTeamAdminTeam1, []string{"host4"}, true},
|
|
{"team 1 admin cannot run mix of team 1 and 2", test.UserTeamAdminTeam1, []string{"host1", "host3"}, true},
|
|
}
|
|
for _, c := range enqueueCmdCases {
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
ctx = test.UserContext(ctx, c.user)
|
|
_, err = svc.EnqueueMDMAppleCommand(ctx, rawB64FreeCmd, c.uuids)
|
|
checkAuthErr(t, err, c.shoudFailWithAuth)
|
|
})
|
|
}
|
|
|
|
// test with a command that requires a premium license
|
|
ctx = test.UserContext(ctx, test.UserAdmin)
|
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierFree})
|
|
rawB64PremiumCmd := base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Command</key>
|
|
<dict>
|
|
<key>RequestType</key>
|
|
<string>%s</string>
|
|
</dict>
|
|
<key>CommandUUID</key>
|
|
<string>uuid</string>
|
|
</dict>
|
|
</plist>`, "DeviceLock")))
|
|
_, err = svc.EnqueueMDMAppleCommand(ctx, rawB64PremiumCmd, []string{"host1"})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, fleet.ErrMissingLicense.Error())
|
|
})
|
|
|
|
cmdUUIDToHostUUIDs := map[string][]string{
|
|
"uuidTm1": {"host1", "host2"},
|
|
"uuidTm2": {"host3"},
|
|
"uuidNoTm": {"host4"},
|
|
"uuidMixTm1Tm2": {"host1", "host3"},
|
|
}
|
|
getResults := func(commandUUID string) ([]*fleet.MDMCommandResult, error) {
|
|
hosts := cmdUUIDToHostUUIDs[commandUUID]
|
|
res := make([]*fleet.MDMCommandResult, 0, len(hosts))
|
|
for _, h := range hosts {
|
|
res = append(res, &fleet.MDMCommandResult{
|
|
HostUUID: h,
|
|
})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
ds.GetMDMAppleCommandResultsFunc = func(ctx context.Context, commandUUID string, hostUUID string) ([]*fleet.MDMCommandResult, error) {
|
|
return getResults(commandUUID)
|
|
}
|
|
|
|
ds.GetMDMCommandPlatformFunc = func(ctx context.Context, commandUUID string) (string, error) {
|
|
return "darwin", nil
|
|
}
|
|
|
|
t.Run("GetMDMAppleCommandResults", func(t *testing.T) {
|
|
cmdResultsCases := []struct {
|
|
desc string
|
|
user *fleet.User
|
|
cmdUUID string
|
|
shoudFailWithAuth bool
|
|
}{
|
|
{"no role", test.UserNoRoles, "uuidTm1", true},
|
|
{"maintainer can view", test.UserMaintainer, "uuidTm1", false},
|
|
{"maintainer can view", test.UserMaintainer, "uuidTm2", false},
|
|
{"maintainer can view", test.UserMaintainer, "uuidNoTm", false},
|
|
{"maintainer can view", test.UserMaintainer, "uuidMixTm1Tm2", false},
|
|
{"observer can view", test.UserObserver, "uuidTm1", false},
|
|
{"observer can view", test.UserObserver, "uuidTm2", false},
|
|
{"observer can view", test.UserObserver, "uuidNoTm", false},
|
|
{"observer can view", test.UserObserver, "uuidMixTm1Tm2", false},
|
|
{"observer+ can view", test.UserObserverPlus, "uuidTm1", false},
|
|
{"observer+ can view", test.UserObserverPlus, "uuidTm2", false},
|
|
{"observer+ can view", test.UserObserverPlus, "uuidNoTm", false},
|
|
{"observer+ can view", test.UserObserverPlus, "uuidMixTm1Tm2", false},
|
|
{"admin can view", test.UserAdmin, "uuidTm1", false},
|
|
{"admin can view", test.UserAdmin, "uuidTm2", false},
|
|
{"admin can view", test.UserAdmin, "uuidNoTm", false},
|
|
{"admin can view", test.UserAdmin, "uuidMixTm1Tm2", false},
|
|
{"tm1 maintainer can view tm1", test.UserTeamMaintainerTeam1, "uuidTm1", false},
|
|
{"tm1 maintainer cannot view tm2", test.UserTeamMaintainerTeam1, "uuidTm2", true},
|
|
{"tm1 maintainer cannot view no team", test.UserTeamMaintainerTeam1, "uuidNoTm", true},
|
|
{"tm1 maintainer cannot view mix", test.UserTeamMaintainerTeam1, "uuidMixTm1Tm2", true},
|
|
{"tm1 observer can view tm1", test.UserTeamObserverTeam1, "uuidTm1", false},
|
|
{"tm1 observer cannot view tm2", test.UserTeamObserverTeam1, "uuidTm2", true},
|
|
{"tm1 observer cannot view no team", test.UserTeamObserverTeam1, "uuidNoTm", true},
|
|
{"tm1 observer cannot view mix", test.UserTeamObserverTeam1, "uuidMixTm1Tm2", true},
|
|
{"tm1 observer+ can view tm1", test.UserTeamObserverPlusTeam1, "uuidTm1", false},
|
|
{"tm1 observer+ cannot view tm2", test.UserTeamObserverPlusTeam1, "uuidTm2", true},
|
|
{"tm1 observer+ cannot view no team", test.UserTeamObserverPlusTeam1, "uuidNoTm", true},
|
|
{"tm1 observer+ cannot view mix", test.UserTeamObserverPlusTeam1, "uuidMixTm1Tm2", true},
|
|
{"tm1 admin can view tm1", test.UserTeamAdminTeam1, "uuidTm1", false},
|
|
{"tm1 admin cannot view tm2", test.UserTeamAdminTeam1, "uuidTm2", true},
|
|
{"tm1 admin cannot view no team", test.UserTeamAdminTeam1, "uuidNoTm", true},
|
|
{"tm1 admin cannot view mix", test.UserTeamAdminTeam1, "uuidMixTm1Tm2", true},
|
|
}
|
|
for _, c := range cmdResultsCases {
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
ctx = test.UserContext(ctx, c.user)
|
|
_, err = svc.GetMDMAppleCommandResults(ctx, c.cmdUUID)
|
|
checkAuthErr(t, err, c.shoudFailWithAuth)
|
|
|
|
// TODO(sarah): move test to shared file
|
|
_, err = svc.GetMDMCommandResults(ctx, c.cmdUUID, "")
|
|
checkAuthErr(t, err, c.shoudFailWithAuth)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("ListMDMAppleCommands", func(t *testing.T) {
|
|
ds.ListMDMAppleCommandsFunc = func(ctx context.Context, tmFilter fleet.TeamFilter, opt *fleet.MDMCommandListOptions) ([]*fleet.MDMAppleCommand, error) {
|
|
return []*fleet.MDMAppleCommand{
|
|
{DeviceID: "no team", TeamID: nil},
|
|
{DeviceID: "tm1", TeamID: ptr.Uint(1)},
|
|
{DeviceID: "tm2", TeamID: ptr.Uint(2)},
|
|
}, nil
|
|
}
|
|
|
|
listCmdsCases := []struct {
|
|
desc string
|
|
user *fleet.User
|
|
want []string // the expected device ids in the results
|
|
shouldFail bool // with forbidden error
|
|
}{
|
|
{"no role", test.UserNoRoles, []string{}, true},
|
|
{"maintainer can view", test.UserMaintainer, []string{"no team", "tm1", "tm2"}, false},
|
|
{"observer can view", test.UserObserver, []string{"no team", "tm1", "tm2"}, false},
|
|
{"observer+ can view", test.UserObserverPlus, []string{"no team", "tm1", "tm2"}, false},
|
|
{"admin can view", test.UserAdmin, []string{"no team", "tm1", "tm2"}, false},
|
|
{"tm1 maintainer can view tm1", test.UserTeamMaintainerTeam1, []string{"tm1"}, false},
|
|
{"tm1 observer can view tm1", test.UserTeamObserverTeam1, []string{"tm1"}, false},
|
|
{"tm1 observer+ can view tm1", test.UserTeamObserverPlusTeam1, []string{"tm1"}, false},
|
|
{"tm1 admin can view tm1", test.UserTeamAdminTeam1, []string{"tm1"}, false},
|
|
}
|
|
for _, c := range listCmdsCases {
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
ctx = test.UserContext(ctx, c.user)
|
|
res, err := svc.ListMDMAppleCommands(ctx, &fleet.MDMCommandListOptions{})
|
|
checkAuthErr(t, err, c.shouldFail)
|
|
if c.shouldFail {
|
|
return
|
|
}
|
|
|
|
got := make([]string, len(res))
|
|
for i, r := range res {
|
|
got[i] = r.DeviceID
|
|
}
|
|
require.Equal(t, c.want, got)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
profUUID := "a" + uuid.NewString()
|
|
testCases := []struct {
|
|
name string
|
|
user *fleet.User
|
|
shouldFailGlobal bool
|
|
shouldFailTeam bool
|
|
}{
|
|
{
|
|
"global admin",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"global maintainer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"global observer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team admin, belongs to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
false,
|
|
},
|
|
{
|
|
"team admin, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team maintainer, belongs to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
false,
|
|
},
|
|
{
|
|
"team maintainer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team observer, belongs to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team observer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"user no roles",
|
|
&fleet.User{ID: 1337},
|
|
true,
|
|
true,
|
|
},
|
|
}
|
|
|
|
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAppleConfigProfile, usesVars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
|
return &cp, nil
|
|
}
|
|
ds.ListMDMAppleConfigProfilesFunc = func(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetMDMAppleProfilesSummaryFunc = func(context.Context, *uint) (*fleet.MDMProfilesSummary, error) {
|
|
return nil, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
|
return &fleet.GroupedCertificateAuthorities{}, nil
|
|
}
|
|
mockGetFuncWithTeamID := func(teamID uint) mock.GetMDMAppleConfigProfileFunc {
|
|
return func(ctx context.Context, puid string) (*fleet.MDMAppleConfigProfile, error) {
|
|
require.Equal(t, profUUID, puid)
|
|
return &fleet.MDMAppleConfigProfile{TeamID: &teamID}, nil
|
|
}
|
|
}
|
|
mockDeleteFuncWithTeamID := func(teamID uint) mock.DeleteMDMAppleConfigProfileFunc {
|
|
return func(ctx context.Context, puid string) error {
|
|
require.Equal(t, profUUID, puid)
|
|
return nil
|
|
}
|
|
}
|
|
mockTeamFuncWithUser := func(u *fleet.User) mock.TeamWithExtrasFunc {
|
|
return func(ctx context.Context, teamID uint) (*fleet.Team, error) {
|
|
if len(u.Teams) > 0 {
|
|
for _, t := range u.Teams {
|
|
if t.ID == teamID {
|
|
return &fleet.Team{ID: teamID, Users: []fleet.TeamUser{{User: *u, Role: t.Role}}}, nil
|
|
}
|
|
}
|
|
}
|
|
return &fleet.Team{}, nil
|
|
}
|
|
}
|
|
mockTeamLiteFunc := func(u *fleet.User) mock.TeamLiteFunc {
|
|
return func(ctx context.Context, teamID uint) (*fleet.TeamLite, error) {
|
|
if len(u.Teams) > 0 {
|
|
for _, t := range u.Teams {
|
|
if t.ID == teamID {
|
|
return &fleet.TeamLite{ID: teamID}, nil
|
|
}
|
|
}
|
|
}
|
|
return &fleet.TeamLite{}, nil
|
|
}
|
|
}
|
|
|
|
checkShouldFail := func(err error, shouldFail bool) {
|
|
if !shouldFail {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
|
}
|
|
}
|
|
|
|
mcBytes := mcBytesForTest("Foo", "Bar", "UUID")
|
|
|
|
for _, tt := range testCases {
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
ds.TeamWithExtrasFunc = mockTeamFuncWithUser(tt.user)
|
|
ds.TeamLiteFunc = mockTeamLiteFunc(tt.user)
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// test authz create new profile (no team)
|
|
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
// test authz create new profile (team 1)
|
|
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, mcBytes, nil, fleet.LabelsIncludeAll)
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
|
|
|
// test authz list profiles (no team)
|
|
_, err = svc.ListMDMAppleConfigProfiles(ctx, 0)
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
// test authz list profiles (team 1)
|
|
_, err = svc.ListMDMAppleConfigProfiles(ctx, 1)
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
|
|
|
// test authz get config profile (no team)
|
|
ds.GetMDMAppleConfigProfileFunc = mockGetFuncWithTeamID(0)
|
|
_, err = svc.GetMDMAppleConfigProfile(ctx, profUUID)
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
// test authz delete config profile (no team)
|
|
ds.DeleteMDMAppleConfigProfileFunc = mockDeleteFuncWithTeamID(0)
|
|
err = svc.DeleteMDMAppleConfigProfile(ctx, profUUID)
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
// test authz get config profile (team 1)
|
|
ds.GetMDMAppleConfigProfileFunc = mockGetFuncWithTeamID(1)
|
|
_, err = svc.GetMDMAppleConfigProfile(ctx, profUUID)
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
|
|
|
// test authz delete config profile (team 1)
|
|
ds.DeleteMDMAppleConfigProfileFunc = mockDeleteFuncWithTeamID(1)
|
|
err = svc.DeleteMDMAppleConfigProfile(ctx, profUUID)
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
|
|
|
// test authz get profiles summary (no team)
|
|
_, err = svc.GetMDMAppleProfilesSummary(ctx, nil)
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
// test authz get profiles summary (no team)
|
|
_, err = svc.GetMDMAppleProfilesSummary(ctx, ptr.Uint(1))
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
identifier := "Bar.$FLEET_VAR_HOST_END_USER_EMAIL_IDP"
|
|
mcBytes := mcBytesForTest("Foo", identifier, "UUID")
|
|
|
|
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAppleConfigProfile, usesVars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
|
require.Equal(t, "Foo", cp.Name)
|
|
assert.Equal(t, identifier, cp.Identifier)
|
|
require.Equal(t, mcBytes, []byte(cp.Mobileconfig))
|
|
return &cp, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
|
return &fleet.GroupedCertificateAuthorities{}, nil
|
|
}
|
|
|
|
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "Foo", cp.Name)
|
|
assert.Equal(t, identifier, cp.Identifier)
|
|
require.Equal(t, mcBytes, []byte(cp.Mobileconfig))
|
|
|
|
// Unsupported Fleet variable
|
|
mcBytes = mcBytesForTest("Foo", identifier, "UUID${FLEET_VAR_BOZO}")
|
|
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
|
assert.ErrorContains(t, err, "Fleet variable")
|
|
|
|
// Test profile with FLEET_SECRET in PayloadDisplayName
|
|
mcBytes = mcBytesForTest("Profile $FLEET_SECRET_PASSWORD", "test.identifier", "UUID")
|
|
_, err = svc.NewMDMAppleConfigProfile(ctx, 0, mcBytes, nil, fleet.LabelsIncludeAll)
|
|
assert.ErrorContains(t, err, "PayloadDisplayName cannot contain FLEET_SECRET variables")
|
|
}
|
|
|
|
func mcBytesForTest(name, identifier, uuid string) []byte {
|
|
return []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array/>
|
|
<key>PayloadDisplayName</key>
|
|
<string>%s</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>%s</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>%s</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</plist>
|
|
`, name, identifier, uuid))
|
|
}
|
|
|
|
func TestBatchSetMDMAppleProfilesWithSecrets(t *testing.T) {
|
|
svc, ctx, _, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
// Test profile with FLEET_SECRET in PayloadDisplayName
|
|
profileWithSecret := mcBytesForTest("Profile $FLEET_SECRET_PASSWORD", "test.identifier", "UUID")
|
|
err := svc.BatchSetMDMAppleProfiles(ctx, nil, nil, [][]byte{profileWithSecret}, false, false)
|
|
assert.ErrorContains(t, err, "PayloadDisplayName cannot contain FLEET_SECRET variables")
|
|
|
|
// Test multiple profiles where one has a secret in PayloadDisplayName
|
|
goodProfile := mcBytesForTest("Good Profile", "good.identifier", "UUID1")
|
|
badProfile := mcBytesForTest("Bad $FLEET_SECRET_KEY Profile", "bad.identifier", "UUID2")
|
|
err = svc.BatchSetMDMAppleProfiles(ctx, nil, nil, [][]byte{goodProfile, badProfile}, false, false)
|
|
assert.ErrorContains(t, err, "PayloadDisplayName cannot contain FLEET_SECRET variables")
|
|
assert.ErrorContains(t, err, "profiles[1]")
|
|
}
|
|
|
|
func TestNewMDMAppleDeclaration(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
// Unsupported Fleet variable
|
|
b := declBytesForTest("D1", "d1content $FLEET_VAR_BOZO")
|
|
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
|
assert.ErrorContains(t, err, "Fleet variable")
|
|
|
|
// decl type missing actual type
|
|
b = declarationForTestWithType("D1", "com.apple.configuration")
|
|
_, err = svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
|
assert.ErrorContains(t, err, "Only configuration declarations (com.apple.configuration.) are supported")
|
|
|
|
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
|
return d, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
|
|
// Good declaration
|
|
b = declBytesForTest("D1", "d1content")
|
|
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, d)
|
|
}
|
|
|
|
func setupAppleMDMServiceWithSkipValidation(t *testing.T, license *fleet.LicenseInfo, skipValidation bool) (fleet.Service, context.Context, *mock.Store) {
|
|
ds := new(mock.Store)
|
|
cfg := config.TestConfig()
|
|
cfg.MDM.AllowAllDeclarations = skipValidation
|
|
testCertPEM, testKeyPEM, err := generateCertWithAPNsTopic()
|
|
require.NoError(t, err)
|
|
config.SetTestMDMConfig(t, &cfg, testCertPEM, testKeyPEM, "../../server/service/testdata")
|
|
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
depStorage := &nanodep_mock.Storage{}
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))),
|
|
)
|
|
|
|
opts := &TestServerOpts{
|
|
FleetConfig: &cfg,
|
|
MDMStorage: mdmStorage,
|
|
DEPStorage: depStorage,
|
|
MDMPusher: pusher,
|
|
License: license,
|
|
ProfileMatcher: nopProfileMatcher{},
|
|
}
|
|
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, opts)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{
|
|
OrgInfo: fleet.OrgInfo{
|
|
OrgName: "Foo Inc.",
|
|
},
|
|
ServerSettings: fleet.ServerSettings{
|
|
ServerURL: "https://foo.example.com",
|
|
},
|
|
MDM: fleet.MDM{
|
|
EnabledAndConfigured: true,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return svc, ctx, ds
|
|
}
|
|
|
|
func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
|
t.Run("forbidden declaration type fails validation by default", func(t *testing.T) {
|
|
svc, ctx, ds := setupAppleMDMServiceWithSkipValidation(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}, false)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
|
return s, nil, nil
|
|
}
|
|
|
|
// Status subscription declarations are forbidden
|
|
b := []byte(`{
|
|
"Type": "com.apple.configuration.management.status-subscriptions",
|
|
"Identifier": "test-status-sub"
|
|
}`)
|
|
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-status-sub", fleet.LabelsIncludeAll)
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "status subscription type")
|
|
})
|
|
|
|
t.Run("forbidden declaration type allowed with skip validation", func(t *testing.T) {
|
|
svc, ctx, ds := setupAppleMDMServiceWithSkipValidation(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}, true)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
|
return s, nil, nil
|
|
}
|
|
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
|
return d, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
|
|
// Status subscription declarations are forbidden but should be allowed when skip is enabled
|
|
b := []byte(`{
|
|
"Type": "com.apple.configuration.management.status-subscriptions",
|
|
"Identifier": "test-status-sub"
|
|
}`)
|
|
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-status-sub", fleet.LabelsIncludeAll)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, d)
|
|
})
|
|
|
|
t.Run("invalid declaration type fails by default", func(t *testing.T) {
|
|
svc, ctx, ds := setupAppleMDMServiceWithSkipValidation(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}, false)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
|
return s, nil, nil
|
|
}
|
|
|
|
// Non com.apple.configuration.* types are invalid
|
|
b := []byte(`{
|
|
"Type": "com.example.invalid",
|
|
"Identifier": "test-invalid"
|
|
}`)
|
|
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-invalid", fleet.LabelsIncludeAll)
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "Only configuration declarations")
|
|
})
|
|
|
|
t.Run("invalid declaration type allowed with skip validation", func(t *testing.T) {
|
|
svc, ctx, ds := setupAppleMDMServiceWithSkipValidation(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}, true)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
|
return s, nil, nil
|
|
}
|
|
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
|
return d, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
|
|
// Invalid type should be allowed when skip is enabled
|
|
b := []byte(`{
|
|
"Type": "com.example.invalid",
|
|
"Identifier": "test-invalid"
|
|
}`)
|
|
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-invalid", fleet.LabelsIncludeAll)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, d)
|
|
})
|
|
|
|
t.Run("OS update declaration blocked without custom OS updates flag", func(t *testing.T) {
|
|
svc, ctx, ds := setupAppleMDMServiceWithSkipValidation(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}, false)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
|
return s, nil, nil
|
|
}
|
|
|
|
b := []byte(`{
|
|
"Type": "com.apple.configuration.softwareupdate.enforcement.specific",
|
|
"Identifier": "test-os-update"
|
|
}`)
|
|
_, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-os-update", fleet.LabelsIncludeAll)
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "OS updates settings")
|
|
})
|
|
|
|
t.Run("OS update declaration allowed with skip validation even without custom OS updates flag", func(t *testing.T) {
|
|
svc, ctx, ds := setupAppleMDMServiceWithSkipValidation(t, &fleet.LicenseInfo{Tier: fleet.TierPremium}, true)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
|
return s, nil, nil
|
|
}
|
|
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
|
return d, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
|
|
b := []byte(`{
|
|
"Type": "com.apple.configuration.softwareupdate.enforcement.specific",
|
|
"Identifier": "test-os-update"
|
|
}`)
|
|
d, err := svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "test-os-update", fleet.LabelsIncludeAll)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, d)
|
|
})
|
|
}
|
|
|
|
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
|
|
func TestHostDetailsMDMProfiles(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
expected := []fleet.HostMDMAppleProfile{
|
|
{HostUUID: "H057-UU1D-1337", Name: "NAME-5", ProfileUUID: "a" + uuid.NewString(), CommandUUID: "CMD-UU1D-5", Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, Detail: ""},
|
|
{HostUUID: "H057-UU1D-1337", Name: "NAME-9", ProfileUUID: "a" + uuid.NewString(), CommandUUID: "CMD-UU1D-8", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, Detail: ""},
|
|
{HostUUID: "H057-UU1D-1337", Name: "NAME-13", ProfileUUID: "a" + uuid.NewString(), CommandUUID: "CMD-UU1D-13", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeRemove, Detail: "Error removing profile"},
|
|
}
|
|
|
|
ds.GetHostMDMAppleProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) {
|
|
if hostUUID == "H057-UU1D-1337" {
|
|
return expected, nil
|
|
}
|
|
return []fleet.HostMDMAppleProfile{}, nil
|
|
}
|
|
ds.HostFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
|
if hostID == uint(42) {
|
|
return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337", Platform: "darwin"}, nil
|
|
}
|
|
return &fleet.Host{ID: hostID, UUID: "WR0N6-UU1D", Platform: "darwin"}, nil
|
|
}
|
|
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
|
if identifier == "h0571d3n71f13r" {
|
|
return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337", Platform: "darwin"}, nil
|
|
}
|
|
return &fleet.Host{ID: uint(21), UUID: "WR0N6-UU1D", Platform: "darwin"}, nil
|
|
}
|
|
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
|
|
return nil
|
|
}
|
|
ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hid uint) ([]*fleet.HostMaintenanceWindow, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
|
return &fleet.HostLockWipeStatus{}, nil
|
|
}
|
|
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
|
|
return nil, nil
|
|
}
|
|
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetHostIssuesLastUpdatedFunc = func(ctx context.Context, hostId uint) (time.Time, error) {
|
|
return time.Time{}, nil
|
|
}
|
|
ds.UpdateHostIssuesFailingPoliciesFunc = func(ctx context.Context, hostIDs []uint) error {
|
|
return nil
|
|
}
|
|
ds.UpdateHostIssuesFailingPoliciesForSingleHostFunc = func(ctx context.Context, hostID uint) error {
|
|
return nil
|
|
}
|
|
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
|
return nil, nil
|
|
}
|
|
ds.GetHostRecoveryLockPasswordStatusFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMRecoveryLockPassword, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
expectedNilSlice := []fleet.HostMDMAppleProfile(nil)
|
|
expectedEmptySlice := []fleet.HostMDMAppleProfile{}
|
|
|
|
cases := []struct {
|
|
name string
|
|
mdmEnabled bool
|
|
hostID *uint
|
|
hostIdentifier *string
|
|
expected *[]fleet.HostMDMAppleProfile
|
|
}{
|
|
{
|
|
name: "TestGetHostMDMProfilesOK",
|
|
mdmEnabled: true,
|
|
hostID: ptr.Uint(42),
|
|
hostIdentifier: nil,
|
|
expected: &expected,
|
|
},
|
|
{
|
|
name: "TestGetHostMDMProfilesEmpty",
|
|
mdmEnabled: true,
|
|
hostID: ptr.Uint(21),
|
|
hostIdentifier: nil,
|
|
expected: &expectedEmptySlice,
|
|
},
|
|
{
|
|
name: "TestGetHostMDMProfilesNil",
|
|
mdmEnabled: false,
|
|
hostID: ptr.Uint(42),
|
|
hostIdentifier: nil,
|
|
expected: &expectedNilSlice,
|
|
},
|
|
{
|
|
name: "TestHostByIdentifierMDMProfilesOK",
|
|
mdmEnabled: true,
|
|
hostID: nil,
|
|
hostIdentifier: ptr.String("h0571d3n71f13r"),
|
|
expected: &expected,
|
|
},
|
|
{
|
|
name: "TestHostByIdentifierMDMProfilesNil",
|
|
mdmEnabled: false,
|
|
hostID: nil,
|
|
hostIdentifier: ptr.String("h0571d3n71f13r"),
|
|
expected: &expectedNilSlice,
|
|
},
|
|
{
|
|
name: "TestHostByIdentifierMDMProfilesEmpty",
|
|
mdmEnabled: true,
|
|
hostID: nil,
|
|
hostIdentifier: ptr.String("4n07h3r1d3n71f13r"),
|
|
expected: &expectedEmptySlice,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: c.mdmEnabled}}, nil
|
|
}
|
|
ds.AppConfigFuncInvoked = false
|
|
ds.HostFuncInvoked = false
|
|
ds.HostByIdentifierFuncInvoked = false
|
|
ds.GetHostMDMAppleProfilesFuncInvoked = false
|
|
|
|
var gotHost *fleet.HostDetail
|
|
if c.hostID != nil {
|
|
h, err := svc.GetHost(ctx, *c.hostID, fleet.HostDetailOptions{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.HostFuncInvoked)
|
|
gotHost = h
|
|
}
|
|
if c.hostIdentifier != nil {
|
|
h, err := svc.HostByIdentifier(ctx, *c.hostIdentifier, fleet.HostDetailOptions{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.HostByIdentifierFuncInvoked)
|
|
gotHost = h
|
|
}
|
|
require.NotNil(t, gotHost)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
|
|
if !c.mdmEnabled {
|
|
var ep []fleet.HostMDMProfile
|
|
switch c.expected {
|
|
case &expectedNilSlice:
|
|
ns := []fleet.HostMDMProfile(nil)
|
|
ep = ns
|
|
case &expectedEmptySlice:
|
|
ep = []fleet.HostMDMProfile{}
|
|
default:
|
|
for _, p := range *c.expected {
|
|
ep = append(ep, p.ToHostMDMProfile(gotHost.Platform))
|
|
}
|
|
}
|
|
require.Equal(t, gotHost.MDM.Profiles, &ep)
|
|
return
|
|
}
|
|
|
|
require.True(t, ds.GetHostMDMAppleProfilesFuncInvoked)
|
|
require.NotNil(t, gotHost.MDM.Profiles)
|
|
ep := make([]fleet.HostMDMProfile, 0, len(*gotHost.MDM.Profiles))
|
|
for _, p := range *c.expected {
|
|
ep = append(ep, p.ToHostMDMProfile(gotHost.Platform))
|
|
}
|
|
require.ElementsMatch(t, ep, *gotHost.MDM.Profiles)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMCommandAuthz(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
|
switch hostID {
|
|
case 1:
|
|
return &fleet.Host{UUID: "test-host-team-1", TeamID: ptr.Uint(1), Platform: "darwin"}, nil
|
|
default:
|
|
return &fleet.Host{UUID: "test-host-no-team", Platform: "darwin"}, nil
|
|
}
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
return &fleet.HostMDMCheckinInfo{Platform: "darwin"}, nil
|
|
}
|
|
|
|
ds.MDMTurnOffFunc = func(ctx context.Context, uuid string) ([]*fleet.User, []fleet.ActivityDetails, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
var mdmEnabled atomic.Bool
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
// This function is called twice during EnqueueMDMAppleCommandRemoveEnrollmentProfile.
|
|
// It first is called to check that the device is enrolled as a pre-condition to enqueueing the
|
|
// command. It is called second time after the command has been enqueued to check whether
|
|
// the device was successfully unenrolled.
|
|
//
|
|
// For each test run, the bool should be initialized to true to simulate an existing device
|
|
// that is initially enrolled to Fleet's MDM.
|
|
enroll := fleet.NanoEnrollment{
|
|
Enabled: mdmEnabled.Swap(!mdmEnabled.Load()),
|
|
}
|
|
return &enroll, nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
user *fleet.User
|
|
shouldFailGlobal bool
|
|
shouldFailTeam bool
|
|
}{
|
|
{
|
|
"global admin",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"global maintainer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"global observer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team admin, belongs to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
false,
|
|
},
|
|
{
|
|
"team admin, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team maintainer, belongs to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
false,
|
|
},
|
|
{
|
|
"team maintainer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team observer, belongs to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"team observer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
true,
|
|
},
|
|
{
|
|
"user no roles",
|
|
&fleet.User{ID: 1337},
|
|
true,
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
|
|
mdmEnabled.Store(true)
|
|
err := svc.UnenrollMDM(ctx, 42) // global host
|
|
if !tt.shouldFailGlobal {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
|
}
|
|
|
|
mdmEnabled.Store(true)
|
|
err = svc.UnenrollMDM(ctx, 1) // host belongs to team 1
|
|
if !tt.shouldFailTeam {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMAuthenticateManualEnrollment(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
mdmLifecycle := mdmlifecycle.New(ds, slog.New(slog.DiscardHandler), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
keyValueStore: redis_key_value.New(redistest.NopRedis()),
|
|
logger: slog.New(slog.DiscardHandler),
|
|
}
|
|
ctx := context.Background()
|
|
uuid, serial, model := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1"
|
|
|
|
ds.MDMAppleUpsertHostFunc = func(ctx context.Context, mdmHost *fleet.Host, fromPersonalEnrollment bool) error {
|
|
require.Equal(t, uuid, mdmHost.UUID)
|
|
require.Equal(t, serial, mdmHost.HardwareSerial)
|
|
require.Equal(t, model, mdmHost.HardwareModel)
|
|
require.False(t, fromPersonalEnrollment)
|
|
return nil
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HardwareSerial: serial,
|
|
DisplayName: fmt.Sprintf("%s (%s)", model, serial),
|
|
InstalledFromDEP: false,
|
|
}, nil
|
|
}
|
|
|
|
ds.MDMResetEnrollmentFunc = func(ctx context.Context, hostUUID string, scepRenewalInProgress bool) error {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return nil
|
|
}
|
|
|
|
err := svc.Authenticate(
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
&mdm.Authenticate{
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
SerialNumber: serial,
|
|
Model: model,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.MDMAppleUpsertHostFuncInvoked)
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
require.True(t, ds.MDMResetEnrollmentFuncInvoked)
|
|
}
|
|
|
|
func TestMDMAuthenticateADE(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
mdmLifecycle := mdmlifecycle.New(ds, slog.New(slog.DiscardHandler), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
keyValueStore: redis_key_value.New(redistest.NopRedis()),
|
|
logger: slog.New(slog.DiscardHandler),
|
|
}
|
|
ctx := context.Background()
|
|
uuid, serial, model := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1"
|
|
|
|
ds.MDMAppleUpsertHostFunc = func(ctx context.Context, mdmHost *fleet.Host, fromPersonalEnrollment bool) error {
|
|
require.Equal(t, uuid, mdmHost.UUID)
|
|
require.Equal(t, serial, mdmHost.HardwareSerial)
|
|
require.Equal(t, model, mdmHost.HardwareModel)
|
|
require.False(t, fromPersonalEnrollment)
|
|
return nil
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HardwareSerial: serial,
|
|
DisplayName: fmt.Sprintf("%s (%s)", model, serial),
|
|
DEPAssignedToFleet: true,
|
|
}, nil
|
|
}
|
|
|
|
ds.MDMResetEnrollmentFunc = func(ctx context.Context, hostUUID string, scepRenewalInProgress bool) error {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return nil
|
|
}
|
|
|
|
err := svc.Authenticate(
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
&mdm.Authenticate{
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
SerialNumber: serial,
|
|
Model: model,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.MDMAppleUpsertHostFuncInvoked)
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
require.True(t, ds.MDMResetEnrollmentFuncInvoked)
|
|
}
|
|
|
|
func TestMDMAuthenticateSCEPRenewal(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
var newActivityInvoked bool
|
|
mdmLifecycle := mdmlifecycle.New(ds, slog.New(slog.DiscardHandler), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error {
|
|
newActivityInvoked = true
|
|
return nil
|
|
})
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
logger: slog.New(slog.DiscardHandler),
|
|
}
|
|
ctx := context.Background()
|
|
uuid, serial, model := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1"
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HardwareSerial: serial,
|
|
DisplayName: fmt.Sprintf("%s (%s)", model, serial),
|
|
SCEPRenewalInProgress: true,
|
|
}, nil
|
|
}
|
|
|
|
ds.MDMResetEnrollmentFunc = func(ctx context.Context, hostUUID string, scepRenewalInProgress bool) error {
|
|
require.Equal(t, uuid, hostUUID)
|
|
require.True(t, scepRenewalInProgress)
|
|
return nil
|
|
}
|
|
ds.MDMAppleUpsertHostFunc = func(ctx context.Context, mdmHost *fleet.Host, fromPersonalEnrollment bool) error {
|
|
require.Equal(t, uuid, mdmHost.UUID)
|
|
require.Equal(t, serial, mdmHost.HardwareSerial)
|
|
require.Equal(t, model, mdmHost.HardwareModel)
|
|
require.False(t, fromPersonalEnrollment)
|
|
return nil
|
|
}
|
|
|
|
err := svc.Authenticate(
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
&mdm.Authenticate{
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
SerialNumber: serial,
|
|
Model: model,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.MDMAppleUpsertHostFuncInvoked)
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
require.False(t, newActivityInvoked)
|
|
require.True(t, ds.MDMResetEnrollmentFuncInvoked)
|
|
}
|
|
|
|
func TestAppleMDMUnenrollment(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
hostOne := &fleet.Host{ID: 1, UUID: "test-host-no-team-2", Platform: "ios"}
|
|
hostGlobal := &fleet.Host{ID: 42, UUID: "test-host-no-team", Platform: "darwin"}
|
|
|
|
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
|
switch hostID {
|
|
case hostOne.ID:
|
|
return hostOne, nil
|
|
case hostGlobal.ID:
|
|
return hostGlobal, nil
|
|
default:
|
|
return nil, errors.New("not found")
|
|
}
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
return &fleet.HostMDMCheckinInfo{Platform: "darwin"}, nil
|
|
}
|
|
|
|
ds.MDMTurnOffFunc = func(ctx context.Context, uuid string) ([]*fleet.User, []fleet.ActivityDetails, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
enrollmentType := mdm.EnrollType(mdm.Device).String()
|
|
if hostUUID == "test-host-no-team-2" {
|
|
enrollmentType = mdm.EnrollType(mdm.UserEnrollmentDevice).String()
|
|
}
|
|
enroll := fleet.NanoEnrollment{
|
|
Enabled: true,
|
|
Type: enrollmentType,
|
|
}
|
|
return &enroll, nil
|
|
}
|
|
|
|
t.Run("Unenrolls macos device", func(t *testing.T) {
|
|
err := svc.UnenrollMDM(ctx, hostGlobal.ID) // global host
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Unenrolls personal ios device", func(t *testing.T) {
|
|
err := svc.UnenrollMDM(ctx, hostOne.ID) // personal host
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestMDMTokenUpdate(t *testing.T) {
|
|
ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ds := new(mock.Store)
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))),
|
|
)
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
|
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
|
|
var newActivityFuncInvoked bool
|
|
mdmLifecycle := mdmlifecycle.New(ds, slog.New(slog.DiscardHandler), func(_ context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
newActivityFuncInvoked = true
|
|
a, ok := activity.(*fleet.ActivityTypeMDMEnrolled)
|
|
require.True(t, ok)
|
|
require.Nil(t, user)
|
|
require.Equal(t, "mdm_enrolled", activity.ActivityName())
|
|
require.NotNil(t, a.HostSerial)
|
|
require.Equal(t, serial, *a.HostSerial)
|
|
require.Nil(t, a.EnrollmentID)
|
|
require.Equal(t, a.HostDisplayName, model)
|
|
require.True(t, a.InstalledFromDEP)
|
|
require.Equal(t, fleet.MDMPlatformApple, a.MDMPlatform)
|
|
return nil
|
|
})
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
commander: cmdr,
|
|
logger: slog.New(slog.DiscardHandler),
|
|
}
|
|
|
|
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: true,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
Platform: "darwin",
|
|
}, nil
|
|
}
|
|
|
|
ds.GetMDMIdPAccountByHostUUIDFunc = func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.MDMIdPAccount{
|
|
UUID: "some-uuid",
|
|
Username: "some-user",
|
|
Email: "some-user@example.com",
|
|
Fullname: "Some User",
|
|
}, nil
|
|
}
|
|
|
|
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
|
return j, nil
|
|
}
|
|
|
|
err := svc.TokenUpdate(
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
require.True(t, ds.NewJobFuncInvoked)
|
|
require.True(t, newActivityFuncInvoked)
|
|
ds.GetHostMDMCheckinInfoFuncInvoked = false
|
|
ds.NewJobFuncInvoked = false
|
|
|
|
// with enrollment reference
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
require.True(t, ds.NewJobFuncInvoked)
|
|
require.True(t, newActivityFuncInvoked)
|
|
|
|
// With AwaitingConfiguration - should check for and enqueue SetupExperience items
|
|
ds.EnqueueSetupExperienceItemsFunc = func(ctx context.Context, hostPlatform, hostPlatformLike string, hostUUID string, teamID uint) (bool, error) {
|
|
require.Equal(t, "darwin", hostPlatformLike)
|
|
require.Equal(t, uuid, hostUUID)
|
|
require.Equal(t, wantTeamID, teamID)
|
|
return true, nil
|
|
}
|
|
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
AwaitingConfiguration: true,
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: true,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
Platform: "darwin",
|
|
MigrationInProgress: true,
|
|
}, nil
|
|
}
|
|
|
|
ds.SetHostMDMMigrationCompletedFunc = func(ctx context.Context, hostID uint) error {
|
|
require.Equal(t, uint(1337), hostID)
|
|
return nil
|
|
}
|
|
|
|
ds.EnqueueSetupExperienceItemsFuncInvoked = false
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
AwaitingConfiguration: true,
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
// Should NOT call the setup experience enqueue function but it should mark the migration complete
|
|
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
require.True(t, ds.SetHostMDMMigrationCompletedFuncInvoked)
|
|
require.True(t, newActivityFuncInvoked)
|
|
|
|
ds.SetHostMDMMigrationCompletedFuncInvoked = false
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
// Should NOT call the setup experience enqueue function but it should mark the migration complete
|
|
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
require.True(t, ds.SetHostMDMMigrationCompletedFuncInvoked)
|
|
require.True(t, newActivityFuncInvoked)
|
|
}
|
|
|
|
func TestMDMTokenUpdateIOS(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))),
|
|
)
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
|
mdmLifecycle := mdmlifecycle.New(ds, slog.New(slog.DiscardHandler), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
commander: cmdr,
|
|
logger: slog.New(slog.DiscardHandler),
|
|
}
|
|
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
|
|
|
|
ds.GetMDMIdPAccountByHostUUIDFunc = func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.MDMIdPAccount{
|
|
UUID: "some-uuid",
|
|
Username: "some-user",
|
|
Email: "some-user@example.com",
|
|
Fullname: "Some User",
|
|
}, nil
|
|
}
|
|
|
|
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
|
|
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
|
return j, nil
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: true,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
Platform: "ios",
|
|
}, nil
|
|
}
|
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil
|
|
}
|
|
|
|
ds.EnqueueSetupExperienceItemsFunc = func(ctx context.Context, hostPlatform, hostPlatformLike string, hostUUID string, teamID uint) (bool, error) {
|
|
require.Equal(t, "ios", hostPlatformLike)
|
|
require.Equal(t, uuid, hostUUID)
|
|
require.Equal(t, wantTeamID, teamID)
|
|
return true, nil
|
|
}
|
|
|
|
// DEP-installed without AwaitingConfiguration - should not enqueue SetupExperience items
|
|
err := svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
|
|
// Non-DEP-installed, non device-type enrollment should not enqueue SetupExperience items
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.User},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
|
|
// Non-DEP-installed without AwaitingConfiguration - should not enqueue SetupExperience items if token count is > 1
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: false,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
Platform: "ios",
|
|
}, nil
|
|
}
|
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 2}, nil
|
|
}
|
|
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
|
|
// Non-DEP-installed without AwaitingConfiguration - should enqueue SetupExperience items if token count is 1
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: false,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
Platform: "ios",
|
|
}, nil
|
|
}
|
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil
|
|
}
|
|
|
|
err = svc.TokenUpdate(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device},
|
|
Params: map[string]string{"enroll_reference": "abcd"},
|
|
},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
}
|
|
|
|
func TestMDMCheckout(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
mdmLifecycle := mdmlifecycle.New(ds, slog.New(slog.DiscardHandler), func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error { return nil })
|
|
var newActivityFuncInvoked bool
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
logger: slog.New(slog.DiscardHandler),
|
|
}
|
|
ctx := context.Background()
|
|
uuid, serial, installedFromDEP, displayName, platform := "ABC-DEF-GHI", "XYZABC", true, "Test's MacBook", "darwin"
|
|
|
|
ds.MDMTurnOffFunc = func(ctx context.Context, hostUUID string) ([]*fleet.User, []fleet.ActivityDetails, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return nil, nil, nil
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HardwareSerial: serial,
|
|
DisplayName: displayName,
|
|
InstalledFromDEP: installedFromDEP,
|
|
Platform: platform,
|
|
}, nil
|
|
}
|
|
|
|
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
svc.newActivityFn = func(
|
|
_ context.Context, user *fleet.User, activity fleet.ActivityDetails,
|
|
) error {
|
|
newActivityFuncInvoked = true
|
|
a, ok := activity.(*fleet.ActivityTypeMDMUnenrolled)
|
|
require.True(t, ok)
|
|
require.Nil(t, user)
|
|
require.Equal(t, "mdm_unenrolled", activity.ActivityName())
|
|
require.Equal(t, serial, a.HostSerial)
|
|
require.Equal(t, displayName, a.HostDisplayName)
|
|
require.True(t, a.InstalledFromDEP)
|
|
require.Equal(t, platform, a.Platform)
|
|
return nil
|
|
}
|
|
|
|
err := svc.CheckOut(
|
|
&mdm.Request{
|
|
Context: ctx,
|
|
EnrollID: &mdm.EnrollID{ID: uuid},
|
|
},
|
|
&mdm.CheckOut{
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.MDMTurnOffFuncInvoked)
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
require.True(t, newActivityFuncInvoked)
|
|
}
|
|
|
|
func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) {
|
|
ctx := context.Background()
|
|
hostUUID := "ABC-DEF-GHI"
|
|
commandUUID := "COMMAND-UUID"
|
|
profileIdentifier := "PROFILE-IDENTIFIER"
|
|
|
|
cases := []struct {
|
|
status string
|
|
requestType string
|
|
errors []mdm.ErrorChain
|
|
want *fleet.HostMDMAppleProfile
|
|
prevRetries uint
|
|
}{
|
|
{
|
|
status: "Acknowledged",
|
|
requestType: "InstallProfile",
|
|
errors: nil,
|
|
want: &fleet.HostMDMAppleProfile{
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
Detail: "",
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
},
|
|
},
|
|
{
|
|
status: "Acknowledged",
|
|
requestType: "RemoveProfile",
|
|
errors: nil,
|
|
want: &fleet.HostMDMAppleProfile{
|
|
Status: &fleet.MDMDeliveryVerifying,
|
|
Detail: "",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
},
|
|
},
|
|
{
|
|
status: "Error",
|
|
requestType: "InstallProfile",
|
|
errors: []mdm.ErrorChain{
|
|
{ErrorCode: 123, ErrorDomain: "testDomain", USEnglishDescription: "testMessage"},
|
|
},
|
|
prevRetries: 0, // expect to retry
|
|
want: &fleet.HostMDMAppleProfile{
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Detail: "",
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
},
|
|
},
|
|
{
|
|
status: "Error",
|
|
requestType: "InstallProfile",
|
|
errors: []mdm.ErrorChain{
|
|
{ErrorCode: 123, ErrorDomain: "testDomain", USEnglishDescription: "testMessage"},
|
|
},
|
|
prevRetries: 1, // expect to fail
|
|
want: &fleet.HostMDMAppleProfile{
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "testDomain (123): testMessage\n",
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
},
|
|
},
|
|
{
|
|
status: "Error",
|
|
requestType: "RemoveProfile",
|
|
errors: []mdm.ErrorChain{
|
|
{ErrorCode: 123, ErrorDomain: "testDomain", USEnglishDescription: "testMessage"},
|
|
{ErrorCode: 321, ErrorDomain: "domainTest", USEnglishDescription: "messageTest"},
|
|
},
|
|
want: &fleet.HostMDMAppleProfile{
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "testDomain (123): testMessage\ndomainTest (321): messageTest\n",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
},
|
|
},
|
|
{
|
|
status: "Error",
|
|
requestType: "RemoveProfile",
|
|
errors: nil,
|
|
want: &fleet.HostMDMAppleProfile{
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "",
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
t.Run(fmt.Sprintf("%s%s-%d", c.requestType, c.status, i), func(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
svc := MDMAppleCheckinAndCommandService{ds: ds, logger: slog.New(slog.DiscardHandler)}
|
|
ds.GetMDMAppleCommandRequestTypeFunc = func(ctx context.Context, targetCmd string) (string, error) {
|
|
require.Equal(t, commandUUID, targetCmd)
|
|
return c.requestType, nil
|
|
}
|
|
ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error {
|
|
c.want.CommandUUID = commandUUID
|
|
c.want.HostUUID = hostUUID
|
|
require.Equal(t, c.want, profile)
|
|
return nil
|
|
}
|
|
ds.GetHostMDMProfileRetryCountByCommandUUIDFunc = func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
|
|
require.Equal(t, hostUUID, host.UUID)
|
|
require.Equal(t, commandUUID, cmdUUID)
|
|
return fleet.HostMDMProfileRetryCount{ProfileIdentifier: profileIdentifier, Retries: c.prevRetries}, nil
|
|
}
|
|
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
|
|
require.Equal(t, hostUUID, host.UUID)
|
|
require.Nil(t, toVerify)
|
|
require.Nil(t, toFail)
|
|
require.ElementsMatch(t, toRetry, []string{profileIdentifier})
|
|
return nil
|
|
}
|
|
|
|
_, err := svc.CommandAndReportResults(
|
|
&mdm.Request{Context: ctx},
|
|
&mdm.CommandResults{
|
|
Enrollment: mdm.Enrollment{UDID: hostUUID},
|
|
CommandUUID: commandUUID,
|
|
Status: c.status,
|
|
ErrorChain: c.errors,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.GetMDMAppleCommandRequestTypeFuncInvoked)
|
|
var shouldCheckCount, shouldRetry, shouldUpdateOrDelete bool
|
|
if c.requestType == "InstallProfile" && c.status == "Error" {
|
|
shouldCheckCount = true
|
|
}
|
|
if shouldCheckCount && c.prevRetries == uint(0) {
|
|
shouldRetry = true
|
|
}
|
|
if c.requestType == "RemoveProfile" || (c.requestType == "InstallProfile" && !shouldRetry) {
|
|
shouldUpdateOrDelete = true
|
|
}
|
|
require.Equal(t, shouldCheckCount, ds.GetHostMDMProfileRetryCountByCommandUUIDFuncInvoked)
|
|
require.Equal(t, shouldRetry, ds.UpdateHostMDMProfilesVerificationFuncInvoked)
|
|
require.Equal(t, shouldUpdateOrDelete, ds.UpdateOrDeleteHostMDMAppleProfileFuncInvoked)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: 1, Name: name}, nil
|
|
}
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: id, Name: "team"}, nil
|
|
}
|
|
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, teamID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
user *fleet.User
|
|
premium bool
|
|
teamID *uint
|
|
teamName *string
|
|
profiles [][]byte
|
|
wantErr string
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
"global admin",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global admin, team",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global maintainer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
false,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global maintainer, team",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global observer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
false,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team admin, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"team admin, DOES belong to team by name",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
nil,
|
|
ptr.String("team"),
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"team admin, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team admin, DOES NOT belong to team by name",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
nil,
|
|
ptr.String("team"),
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team maintainer, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"team maintainer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team observer, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team observer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"user no roles",
|
|
&fleet.User{ID: 1337},
|
|
false,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team id with free license",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
ptr.Uint(1),
|
|
nil,
|
|
nil,
|
|
ErrMissingLicense.Error(),
|
|
},
|
|
{
|
|
"team name with free license",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
nil,
|
|
ptr.String("team"),
|
|
nil,
|
|
ErrMissingLicense.Error(),
|
|
},
|
|
{
|
|
"team id and name specified",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
ptr.Uint(1),
|
|
ptr.String("team"),
|
|
nil,
|
|
"cannot specify both team_id and team_name",
|
|
},
|
|
{
|
|
"duplicate profile name",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
[][]byte{
|
|
mobileconfigForTest("N1", "I1"),
|
|
mobileconfigForTest("N1", "I2"),
|
|
},
|
|
`More than one configuration profile have the same name (PayloadDisplayName): "N1"`,
|
|
},
|
|
{
|
|
"duplicate profile identifier",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
ptr.Uint(1),
|
|
nil,
|
|
[][]byte{
|
|
mobileconfigForTest("N1", "I1"),
|
|
mobileconfigForTest("N2", "I2"),
|
|
mobileconfigForTest("N3", "I1"),
|
|
},
|
|
`More than one configuration profile have the same identifier (PayloadIdentifier): "I1"`,
|
|
},
|
|
{
|
|
"no duplicates",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
nil,
|
|
nil,
|
|
[][]byte{
|
|
mobileconfigForTest("N1", "I1"),
|
|
mobileconfigForTest("N2", "I2"),
|
|
mobileconfigForTest("N3", "I3"),
|
|
},
|
|
``,
|
|
},
|
|
{
|
|
"unsupported payload type",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
nil,
|
|
nil,
|
|
[][]byte{[]byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>Enable</key>
|
|
<string>On</string>
|
|
<key>PayloadDisplayName</key>
|
|
<string>FileVault 2</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.apple.MCX.FileVault2.A5874654-D6BA-4649-84B5-43847953B369</string>
|
|
<key>PayloadType</key>
|
|
<string>%s</string>
|
|
<key>PayloadUUID</key>
|
|
<string>A5874654-D6BA-4649-84B5-43847953B369</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</array>
|
|
<key>PayloadDisplayName</key>
|
|
<string>Config Profile Name</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.example.config.FE42D0A2-DBA9-4B72-BC67-9288665B8D59</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>FE42D0A2-DBA9-4B72-BC67-9288665B8D59</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</plist>`, mobileconfig.FleetFileVaultPayloadType))},
|
|
mobileconfig.DiskEncryptionProfileRestrictionErrMsg,
|
|
},
|
|
{
|
|
"uses a Fleet Variable",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
nil,
|
|
nil,
|
|
[][]byte{[]byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>Username</key>
|
|
<string>$FLEET_VAR_HOST_END_USER_IDP_USERNAME</string>
|
|
</dict>
|
|
</array>
|
|
<key>PayloadDisplayName</key>
|
|
<string>Config Profile Name</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>com.example.config.FE42D0A2-DBA9-4B72-BC67-9288665B8D59</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>FE42D0A2-DBA9-4B72-BC67-9288665B8D59</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</plist>`)},
|
|
`profile variables are not supported by this deprecated endpoint`,
|
|
},
|
|
}
|
|
for name := range fleetmdm.FleetReservedProfileNames() {
|
|
testCases = append(testCases,
|
|
testCase{
|
|
"reserved payload outer name " + name,
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
nil,
|
|
nil,
|
|
[][]byte{mobileconfigForTest(name, "I1")},
|
|
name,
|
|
},
|
|
testCase{
|
|
"reserved payload inner name " + name,
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
nil,
|
|
nil,
|
|
[][]byte{mobileconfigForTestWithContent("N1", "I1", "I1", "PayloadType", name)},
|
|
name,
|
|
},
|
|
)
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer func() { ds.BatchSetMDMAppleProfilesFuncInvoked = false }()
|
|
|
|
// prepare the context with the user and license
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
tier := fleet.TierFree
|
|
if tt.premium {
|
|
tier = fleet.TierPremium
|
|
}
|
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
|
|
|
|
err := svc.BatchSetMDMAppleProfiles(ctx, tt.teamID, tt.teamName, tt.profiles, false, false)
|
|
if tt.wantErr == "" {
|
|
require.NoError(t, err)
|
|
require.True(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
require.False(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
|
|
svc, ctx, ds, svcOpts := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: 1, Name: name}, nil
|
|
}
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: id, Name: "team"}, nil
|
|
}
|
|
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, teamID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
|
|
return nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
// dry run doesn't call methods that save stuff in the db
|
|
err := svc.BatchSetMDMAppleProfiles(ctx, nil, nil, [][]byte{}, true, false)
|
|
require.NoError(t, err)
|
|
require.False(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
|
|
require.False(t, ds.BulkSetPendingMDMHostProfilesFuncInvoked)
|
|
require.False(t, svcOpts.ActivityMock.NewActivityFuncInvoked)
|
|
|
|
// skipping bulk set only skips that method
|
|
err = svc.BatchSetMDMAppleProfiles(ctx, nil, nil, [][]byte{}, false, true)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.BatchSetMDMAppleProfilesFuncInvoked)
|
|
require.False(t, ds.BulkSetPendingMDMHostProfilesFuncInvoked)
|
|
require.True(t, svcOpts.ActivityMock.NewActivityFuncInvoked)
|
|
}
|
|
|
|
func TestUpdateMDMAppleSettings(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: id, Name: "team"}, nil
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return team, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, appConfig *fleet.AppConfig) error {
|
|
return nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
user *fleet.User
|
|
premium bool
|
|
teamID *uint
|
|
wantErr string
|
|
}{
|
|
{
|
|
"global admin",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
nil,
|
|
fleet.ErrMissingLicense.Error(),
|
|
},
|
|
{
|
|
"global admin premium",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global admin, team",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
true,
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"global maintainer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
false,
|
|
nil,
|
|
fleet.ErrMissingLicense.Error(),
|
|
},
|
|
{
|
|
"global maintainer premium",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
true,
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global maintainer, team",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
true,
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"global observer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
true,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team admin, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"team admin, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team maintainer, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"team maintainer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team observer, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team observer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
|
|
true,
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"user no roles",
|
|
&fleet.User{ID: 1337},
|
|
true,
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team id with free license",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
false,
|
|
ptr.Uint(1),
|
|
fleet.ErrMissingLicense.Error(),
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// prepare the context with the user and license
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
tier := fleet.TierFree
|
|
if tt.premium {
|
|
tier = fleet.TierPremium
|
|
}
|
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: tier})
|
|
|
|
err := svc.UpdateMDMDiskEncryption(ctx, tt.teamID, nil, nil)
|
|
if tt.wantErr == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateMDMAppleSetup(t *testing.T) {
|
|
setupTest := func(tier string) (fleet.Service, context.Context, *mock.Store) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: tier})
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: id, Name: "team"}, nil
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return team, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
|
}
|
|
ds.SaveAppConfigFunc = func(ctx context.Context, appConfig *fleet.AppConfig) error {
|
|
return nil
|
|
}
|
|
return svc, ctx, ds
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
user *fleet.User
|
|
teamID *uint
|
|
wantErr string
|
|
}
|
|
// TODO: Add tests for gitops and observer plus roles? (Settings endpoint test above may also need to be updated)
|
|
|
|
t.Run("FreeTier", func(t *testing.T) {
|
|
freeTestCases := []testCase{
|
|
{
|
|
"global admin",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
nil,
|
|
"Requires Fleet Premium license",
|
|
},
|
|
{
|
|
"global maintainer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
nil,
|
|
"Requires Fleet Premium license",
|
|
},
|
|
{
|
|
"team id with free license",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
ptr.Uint(1),
|
|
"Requires Fleet Premium license",
|
|
},
|
|
}
|
|
svc, ctx, _ := setupTest(fleet.TierFree)
|
|
for _, tt := range freeTestCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// prepare the context with the user and license
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
err := svc.UpdateMDMAppleSetup(ctx, fleet.MDMAppleSetupPayload{TeamID: tt.teamID})
|
|
if tt.wantErr == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
})
|
|
t.Run("PremiumTier", func(t *testing.T) {
|
|
premiumTestCases := []testCase{
|
|
{
|
|
"global admin premium",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global admin, team",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"global maintainer premium",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
"global maintainer, team",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"global observer",
|
|
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team admin, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"team admin, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}},
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team maintainer, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
|
ptr.Uint(1),
|
|
"",
|
|
},
|
|
{
|
|
"team maintainer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}},
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team observer, DOES belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"team observer, DOES NOT belong to team",
|
|
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}},
|
|
ptr.Uint(1),
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
{
|
|
"user no roles",
|
|
&fleet.User{ID: 1337},
|
|
nil,
|
|
authz.ForbiddenErrorMessage,
|
|
},
|
|
}
|
|
svc, ctx, _ := setupTest(fleet.TierPremium)
|
|
for _, tt := range premiumTestCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// prepare the context with the user and license
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
err := svc.UpdateMDMAppleSetup(ctx, fleet.MDMAppleSetupPayload{TeamID: tt.teamID})
|
|
if tt.wantErr == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
|
|
ctx := context.Background()
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
ds := new(mock.Store)
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(slog.New(slog.DiscardHandler)),
|
|
)
|
|
mdmConfig := config.MDMConfig{
|
|
AppleSCEPCert: "./testdata/server.pem",
|
|
AppleSCEPKey: "./testdata/server.key",
|
|
}
|
|
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
_, pemCert, pemKey, err := mdmConfig.AppleSCEP()
|
|
require.NoError(t, err)
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetCACert: {Value: pemCert},
|
|
fleet.MDMAssetCAKey: {Value: pemKey},
|
|
}, nil
|
|
}
|
|
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
|
hostUUID1, hostUUID2 := "ABC-DEF", "GHI-JKL"
|
|
hostUUID1UserEnrollment := hostUUID1 + ":user"
|
|
contents1 := []byte("test-content-1")
|
|
expectedContents1 := []byte("test-content-1") // used for Fleet variable substitution
|
|
contents2 := []byte("test-content-2")
|
|
contents4 := []byte("test-content-4")
|
|
contents5 := []byte("test-contents-5")
|
|
contents7 := []byte("test-contents-7")
|
|
|
|
p1, p2, p3, p4, p5, p6, p7 := "a"+uuid.NewString(), "a"+uuid.NewString(), "a"+uuid.NewString(), "a"+uuid.NewString(), "a"+uuid.NewString(), "a"+uuid.NewString(), "a"+uuid.NewString()
|
|
baseProfilesToInstall := []*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: p1, ProfileIdentifier: "com.add.profile", HostUUID: hostUUID1, Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: p2, ProfileIdentifier: "com.add.profile.two", HostUUID: hostUUID1, Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: p2, ProfileIdentifier: "com.add.profile.two", HostUUID: hostUUID2, Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: p4, ProfileIdentifier: "com.add.profile.four", HostUUID: hostUUID2, Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: p5, ProfileIdentifier: "com.add.profile.five", HostUUID: hostUUID1, Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: p5, ProfileIdentifier: "com.add.profile.five", HostUUID: hostUUID2, Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: p7, ProfileIdentifier: "com.add.profile.seven", HostUUID: hostUUID1, Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: p7, ProfileIdentifier: "com.add.profile.seven", HostUUID: hostUUID2, Scope: fleet.PayloadScopeUser, HostPlatform: "ios"},
|
|
}
|
|
baseProfilesToRemove := []*fleet.MDMAppleProfilePayload{
|
|
{ProfileUUID: p3, ProfileIdentifier: "com.remove.profile", HostUUID: hostUUID1, Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: p3, ProfileIdentifier: "com.remove.profile", HostUUID: hostUUID2, Scope: fleet.PayloadScopeSystem},
|
|
{ProfileUUID: p6, ProfileIdentifier: "com.remove.profile.six", HostUUID: hostUUID1, Scope: fleet.PayloadScopeUser},
|
|
{ProfileUUID: p6, ProfileIdentifier: "com.remove.profile.six", HostUUID: hostUUID2, Scope: fleet.PayloadScopeUser},
|
|
}
|
|
ds.ListMDMAppleProfilesToInstallAndRemoveFunc = func(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, []*fleet.MDMAppleProfilePayload, error) {
|
|
return baseProfilesToInstall, baseProfilesToRemove, nil
|
|
}
|
|
|
|
ds.GetMDMAppleProfilesContentsFunc = func(ctx context.Context, profileUUIDs []string) (map[string]mobileconfig.Mobileconfig, error) {
|
|
require.ElementsMatch(t, []string{p1, p2, p4, p5, p7}, profileUUIDs)
|
|
// only those profiles that are to be installed
|
|
return map[string]mobileconfig.Mobileconfig{
|
|
p1: contents1,
|
|
p2: contents2,
|
|
p4: contents4,
|
|
p5: contents5,
|
|
p7: contents7,
|
|
}, nil
|
|
}
|
|
|
|
ds.BulkDeleteMDMAppleHostsConfigProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error {
|
|
require.ElementsMatch(t, payload, []*fleet.MDMAppleProfilePayload{{ProfileUUID: p6, ProfileIdentifier: "com.remove.profile.six", HostUUID: hostUUID2, Scope: fleet.PayloadScopeUser}})
|
|
return nil
|
|
}
|
|
|
|
ds.GetNanoMDMUserEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
if hostUUID == hostUUID1 {
|
|
return &fleet.NanoEnrollment{
|
|
ID: hostUUID1UserEnrollment,
|
|
DeviceID: hostUUID1,
|
|
Type: "User",
|
|
Enabled: true,
|
|
TokenUpdateTally: 1,
|
|
}, nil
|
|
}
|
|
// hostUUID2 has no user enrollment
|
|
assert.Equal(t, hostUUID2, hostUUID)
|
|
return nil, nil
|
|
}
|
|
|
|
mdmStorage.BulkDeleteHostUserCommandsWithoutResultsFunc = func(ctx context.Context, commandToIDs map[string][]string) error {
|
|
require.Empty(t, commandToIDs)
|
|
return nil
|
|
}
|
|
|
|
var enqueueFailForOp fleet.MDMOperationType
|
|
var mu sync.Mutex
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
|
|
require.NotNil(t, cmd)
|
|
require.NotEmpty(t, cmd.CommandUUID)
|
|
|
|
switch cmd.Command.Command.RequestType {
|
|
case "InstallProfile":
|
|
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
// the p7 library doesn't support concurrent calls to Parse
|
|
mu.Lock()
|
|
pk7, err := pkcs7.Parse(fullCmd.Command.InstallProfile.Payload)
|
|
mu.Unlock()
|
|
require.NoError(t, err)
|
|
|
|
if !bytes.Equal(pk7.Content, expectedContents1) && !bytes.Equal(pk7.Content, contents2) &&
|
|
!bytes.Equal(pk7.Content, contents4) && !bytes.Equal(pk7.Content, contents5) && !bytes.Equal(pk7.Content, contents7) {
|
|
require.Failf(t, "profile contents don't match", "expected to contain %s, %s or %s but got %s",
|
|
expectedContents1, contents2, contents4, pk7.Content)
|
|
}
|
|
|
|
// may be called for a single host or both
|
|
if len(id) == 2 {
|
|
if bytes.Equal(pk7.Content, contents5) || bytes.Equal(pk7.Content, contents7) {
|
|
require.ElementsMatch(t, []string{hostUUID1UserEnrollment, hostUUID2}, id)
|
|
} else {
|
|
require.ElementsMatch(t, []string{hostUUID1, hostUUID2}, id)
|
|
}
|
|
} else {
|
|
require.Len(t, id, 1)
|
|
}
|
|
|
|
case "RemoveProfile":
|
|
if len(id) == 1 {
|
|
require.Equal(t, hostUUID1UserEnrollment, id[0])
|
|
} else {
|
|
require.ElementsMatch(t, []string{hostUUID1, hostUUID2}, id)
|
|
}
|
|
require.Contains(t, string(cmd.Raw), "com.remove.profile")
|
|
}
|
|
switch {
|
|
case enqueueFailForOp == fleet.MDMOperationTypeInstall && cmd.Command.Command.RequestType == "InstallProfile":
|
|
return nil, errors.New("enqueue error")
|
|
case enqueueFailForOp == fleet.MDMOperationTypeRemove && cmd.Command.Command.RequestType == "RemoveProfile":
|
|
return nil, errors.New("enqueue error")
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
mdmStorage.RetrievePushInfoFunc = func(ctx context.Context, tokens []string) (map[string]*mdm.Push, error) {
|
|
res := make(map[string]*mdm.Push, len(tokens))
|
|
for _, t := range tokens {
|
|
res[t] = &mdm.Push{
|
|
PushMagic: "",
|
|
Token: []byte(t),
|
|
Topic: "",
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
mdmStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
|
|
cert, err := tls.LoadX509KeyPair("testdata/server.pem", "testdata/server.key")
|
|
return &cert, "", err
|
|
}
|
|
mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
certPEM, err := os.ReadFile("./testdata/server.pem")
|
|
require.NoError(t, err)
|
|
keyPEM, err := os.ReadFile("./testdata/server.key")
|
|
require.NoError(t, err)
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetCACert: {Value: certPEM},
|
|
fleet.MDMAssetCAKey: {Value: keyPEM},
|
|
}, nil
|
|
}
|
|
|
|
var failedCall bool
|
|
var failedCheck func([]*fleet.MDMAppleBulkUpsertHostProfilePayload)
|
|
ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error {
|
|
if failedCall {
|
|
failedCheck(payload)
|
|
return nil
|
|
}
|
|
|
|
// next call will be failed call, until reset
|
|
failedCall = true
|
|
|
|
// first time it is called, it is to set the status to pending and all
|
|
// host profiles have a command uuid
|
|
cmdUUIDByProfileUUIDInstall := make(map[string]string)
|
|
cmdUUIDByProfileUUIDRemove := make(map[string]string)
|
|
copies := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(payload))
|
|
for i, p := range payload {
|
|
// clear the command UUID (in a copy so that it does not affect the
|
|
// pointed-to struct) from the payload for the subsequent checks
|
|
copyp := *p
|
|
copyp.CommandUUID = ""
|
|
copies[i] = ©p
|
|
|
|
// Host with no user enrollment, so install fails
|
|
if p.HostUUID == hostUUID2 && (p.ProfileUUID == p5 || p.ProfileUUID == p7) {
|
|
continue
|
|
}
|
|
|
|
if p.OperationType == fleet.MDMOperationTypeInstall {
|
|
existing, ok := cmdUUIDByProfileUUIDInstall[p.ProfileUUID]
|
|
if ok {
|
|
require.Equal(t, existing, p.CommandUUID)
|
|
} else {
|
|
cmdUUIDByProfileUUIDInstall[p.ProfileUUID] = p.CommandUUID
|
|
}
|
|
} else {
|
|
require.Equal(t, fleet.MDMOperationTypeRemove, p.OperationType)
|
|
existing, ok := cmdUUIDByProfileUUIDRemove[p.ProfileUUID]
|
|
if ok {
|
|
require.Equal(t, existing, p.CommandUUID)
|
|
} else {
|
|
cmdUUIDByProfileUUIDRemove[p.ProfileUUID] = p.CommandUUID
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
{
|
|
ProfileUUID: p1,
|
|
ProfileIdentifier: "com.add.profile",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p2,
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p2,
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p3,
|
|
ProfileIdentifier: "com.remove.profile",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p3,
|
|
ProfileIdentifier: "com.remove.profile",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p4,
|
|
ProfileIdentifier: "com.add.profile.four",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
// This host has a user enrollment so the profile is sent to it
|
|
{
|
|
ProfileUUID: p5,
|
|
ProfileIdentifier: "com.add.profile.five",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
// This host has no user enrollment so the profile is errored
|
|
{
|
|
ProfileUUID: p5,
|
|
ProfileIdentifier: "com.add.profile.five",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Detail: "This setting couldn't be enforced because the user channel doesn't exist for this host. Currently, Fleet creates the user channel for hosts that automatically enroll.",
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
// This host has a user enrollment so the profile is removed from it
|
|
{
|
|
ProfileUUID: p6,
|
|
ProfileIdentifier: "com.remove.profile.six",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
// Note that host2 has no user enrollment so the profile is not marked for removal
|
|
// from it
|
|
{
|
|
ProfileUUID: p7,
|
|
ProfileIdentifier: "com.add.profile.seven",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
{
|
|
ProfileUUID: p7,
|
|
ProfileIdentifier: "com.add.profile.seven",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "This setting couldn't be enforced because the user channel isn't available on iOS and iPadOS hosts.",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
}, copies)
|
|
return nil
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.ServerSettings.ServerURL = "https://test.example.com"
|
|
appCfg.MDM.EnabledAndConfigured = true
|
|
return appCfg, nil
|
|
}
|
|
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
|
return &fleet.GroupedCertificateAuthorities{}, nil
|
|
}
|
|
|
|
ds.BulkUpsertMDMAppleConfigProfilesFunc = func(ctx context.Context, p []*fleet.MDMAppleConfigProfile) error {
|
|
return nil
|
|
}
|
|
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return []*fleet.EnrollSecret{}, nil
|
|
}
|
|
|
|
checkAndReset := func(t *testing.T, want bool, invoked *bool) {
|
|
if want {
|
|
assert.True(t, *invoked)
|
|
} else {
|
|
assert.False(t, *invoked)
|
|
}
|
|
*invoked = false
|
|
}
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
var failedCount int
|
|
failedCall = false
|
|
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
|
|
failedCount++
|
|
require.Len(t, payload, 0)
|
|
}
|
|
err := ReconcileAppleProfiles(ctx, ds, cmdr, slog.New(slog.DiscardHandler))
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, failedCount)
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallAndRemoveFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetNanoMDMUserEnrollmentFuncInvoked)
|
|
})
|
|
|
|
t.Run("fail enqueue remove ops", func(t *testing.T) {
|
|
var failedCount int
|
|
failedCall = false
|
|
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
|
|
failedCount++
|
|
require.Len(t, payload, 3) // the 3 remove ops
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
{
|
|
ProfileUUID: p3,
|
|
ProfileIdentifier: "com.remove.profile",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p3,
|
|
ProfileIdentifier: "com.remove.profile",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p6,
|
|
ProfileIdentifier: "com.remove.profile.six",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeRemove,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
}, payload)
|
|
}
|
|
|
|
enqueueFailForOp = fleet.MDMOperationTypeRemove
|
|
err := ReconcileAppleProfiles(ctx, ds, cmdr, slog.New(slog.DiscardHandler))
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, failedCount)
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallAndRemoveFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetNanoMDMUserEnrollmentFuncInvoked)
|
|
})
|
|
|
|
t.Run("fail enqueue install ops", func(t *testing.T) {
|
|
var failedCount int
|
|
failedCall = false
|
|
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
|
|
failedCount++
|
|
|
|
require.Len(t, payload, 6) // the 6 install ops
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
{
|
|
ProfileUUID: p1,
|
|
ProfileIdentifier: "com.add.profile",
|
|
HostUUID: hostUUID1, OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p2,
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
HostUUID: hostUUID1, OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p2,
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p4,
|
|
ProfileIdentifier: "com.add.profile.four",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p5,
|
|
ProfileIdentifier: "com.add.profile.five",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
{
|
|
ProfileUUID: p7,
|
|
ProfileIdentifier: "com.add.profile.seven",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: nil,
|
|
CommandUUID: "",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
}, payload)
|
|
}
|
|
|
|
enqueueFailForOp = fleet.MDMOperationTypeInstall
|
|
err := ReconcileAppleProfiles(ctx, ds, cmdr, slog.New(slog.DiscardHandler))
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, failedCount)
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallAndRemoveFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetNanoMDMUserEnrollmentFuncInvoked)
|
|
})
|
|
|
|
// Zero profiles to remove
|
|
ds.ListMDMAppleProfilesToInstallAndRemoveFunc = func(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, []*fleet.MDMAppleProfilePayload, error) {
|
|
return baseProfilesToInstall, nil, nil
|
|
}
|
|
ds.BulkDeleteMDMAppleHostsConfigProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error {
|
|
require.Empty(t, payload)
|
|
return nil
|
|
}
|
|
ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error {
|
|
if failedCall {
|
|
failedCheck(payload)
|
|
return nil
|
|
}
|
|
|
|
// next call will be failed call, until reset
|
|
failedCall = true
|
|
|
|
// first time it is called, it is to set the status to pending and all
|
|
// host profiles have a command uuid
|
|
cmdUUIDByProfileUUIDInstall := make(map[string]string)
|
|
cmdUUIDByProfileUUIDRemove := make(map[string]string)
|
|
copies := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(payload))
|
|
for i, p := range payload {
|
|
// clear the command UUID (in a copy so that it does not affect the
|
|
// pointed-to struct) from the payload for the subsequent checks
|
|
copyp := *p
|
|
copyp.CommandUUID = ""
|
|
copies[i] = ©p
|
|
|
|
// Host with no user enrollment, so install fails
|
|
if p.HostUUID == hostUUID2 && (p.ProfileUUID == p5 || p.ProfileUUID == p7) {
|
|
continue
|
|
}
|
|
|
|
if p.OperationType == fleet.MDMOperationTypeInstall {
|
|
existing, ok := cmdUUIDByProfileUUIDInstall[p.ProfileUUID]
|
|
if ok {
|
|
require.Equal(t, existing, p.CommandUUID)
|
|
} else {
|
|
cmdUUIDByProfileUUIDInstall[p.ProfileUUID] = p.CommandUUID
|
|
}
|
|
} else {
|
|
require.Equal(t, fleet.MDMOperationTypeRemove, p.OperationType)
|
|
existing, ok := cmdUUIDByProfileUUIDRemove[p.ProfileUUID]
|
|
if ok {
|
|
require.Equal(t, existing, p.CommandUUID)
|
|
} else {
|
|
cmdUUIDByProfileUUIDRemove[p.ProfileUUID] = p.CommandUUID
|
|
}
|
|
}
|
|
}
|
|
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
{
|
|
ProfileUUID: p1,
|
|
ProfileIdentifier: "com.add.profile",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p2,
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p2,
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p4,
|
|
ProfileIdentifier: "com.add.profile.four",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeSystem,
|
|
},
|
|
{
|
|
ProfileUUID: p5,
|
|
ProfileIdentifier: "com.add.profile.five",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
// This host has no user enrollment so the profile is sent to the device enrollment
|
|
{
|
|
ProfileUUID: p5,
|
|
ProfileIdentifier: "com.add.profile.five",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "This setting couldn't be enforced because the user channel doesn't exist for this host. Currently, Fleet creates the user channel for hosts that automatically enroll.",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
{
|
|
ProfileUUID: p7,
|
|
ProfileIdentifier: "com.add.profile.seven",
|
|
HostUUID: hostUUID1,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryPending,
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
{
|
|
ProfileUUID: p7,
|
|
ProfileIdentifier: "com.add.profile.seven",
|
|
HostUUID: hostUUID2,
|
|
OperationType: fleet.MDMOperationTypeInstall,
|
|
Status: &fleet.MDMDeliveryFailed,
|
|
Detail: "This setting couldn't be enforced because the user channel isn't available on iOS and iPadOS hosts.",
|
|
Scope: fleet.PayloadScopeUser,
|
|
},
|
|
}, copies)
|
|
return nil
|
|
}
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.ServerSettings.ServerURL = "https://test.example.com"
|
|
appCfg.MDM.EnabledAndConfigured = true
|
|
return appCfg, nil
|
|
}
|
|
|
|
// TODO(hca): Mock this to enable NDES?
|
|
// ds.GetAllCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) ([]*fleet.CertificateAuthority, error) {
|
|
// return []*fleet.CertificateAuthority{}, nil
|
|
// }
|
|
|
|
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ds.BulkUpsertMDMManagedCertificatesFunc = func(ctx context.Context, payload []*fleet.MDMManagedCertificate) error {
|
|
assert.Empty(t, payload)
|
|
return nil
|
|
}
|
|
|
|
// TODO(hca): ask Magnus where/how new tests cover the CA portion of this test
|
|
t.Run("replace $FLEET_VAR_"+string(fleet.FleetVarNDESSCEPProxyURL), func(t *testing.T) {
|
|
var upsertCount int
|
|
failedCall = false
|
|
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
|
|
upsertCount++
|
|
if upsertCount == 1 {
|
|
// We update the profile with a new command UUID
|
|
assert.Len(t, payload, 1, "at upsertCount %d", upsertCount)
|
|
} else {
|
|
assert.Len(t, payload, 0, "at upsertCount %d", upsertCount)
|
|
}
|
|
}
|
|
enqueueFailForOp = ""
|
|
newContents := "$FLEET_VAR_" + fleet.FleetVarNDESSCEPProxyURL
|
|
originalContents1 := contents1
|
|
originalExpectedContents1 := expectedContents1
|
|
contents1 = []byte(newContents)
|
|
expectedContents1 = []byte("https://test.example.com" + apple_mdm.SCEPProxyPath + url.QueryEscape(fmt.Sprintf("%s,%s,NDES", hostUUID1, p1)))
|
|
t.Cleanup(func() {
|
|
contents1 = originalContents1
|
|
expectedContents1 = originalExpectedContents1
|
|
})
|
|
err := ReconcileAppleProfiles(ctx, ds, cmdr, slog.New(slog.DiscardHandler))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, upsertCount)
|
|
// checkAndReset(t, true, &ds.GetAllCertificateAuthoritiesFuncInvoked)
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallAndRemoveFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetNanoMDMUserEnrollmentFuncInvoked)
|
|
})
|
|
|
|
// TODO(hca): ask Magnus where/how new tests cover the CA portion of this test
|
|
t.Run("preprocessor fails on $FLEET_VAR_"+string(fleet.FleetVarHostEndUserEmailIDP), func(t *testing.T) {
|
|
var failedCount int
|
|
failedCall = false
|
|
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
|
|
failedCount++
|
|
require.Len(t, payload, 8)
|
|
}
|
|
enqueueFailForOp = ""
|
|
newContents := "$FLEET_VAR_" + fleet.FleetVarHostEndUserEmailIDP
|
|
originalContents1 := contents1
|
|
contents1 = []byte(newContents)
|
|
t.Cleanup(func() {
|
|
contents1 = originalContents1
|
|
})
|
|
ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID string, source string) ([]string, error) {
|
|
return nil, errors.New("GetHostEmailsFuncError")
|
|
}
|
|
err := ReconcileAppleProfiles(ctx, ds, cmdr, slog.New(slog.Default().Handler()))
|
|
assert.ErrorContains(t, err, "GetHostEmailsFuncError")
|
|
// checkAndReset(t, true, &ds.GetAllCertificateAuthoritiesFuncInvoked)
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallAndRemoveFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetNanoMDMUserEnrollmentFuncInvoked)
|
|
})
|
|
|
|
// TODO(hca): ask Magnus where/how new tests cover the CA portion of this test
|
|
t.Run("bad $FLEET_VAR", func(t *testing.T) {
|
|
var failedCount int
|
|
failedCall = false
|
|
var hostUUIDs []string
|
|
failedCheck = func(payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) {
|
|
if len(payload) > 0 {
|
|
failedCount++
|
|
}
|
|
for _, p := range payload {
|
|
assert.Equal(t, fleet.MDMDeliveryFailed, *p.Status)
|
|
assert.Contains(t, p.Detail, "FLEET_VAR_BOZO")
|
|
for i, hu := range hostUUIDs {
|
|
if hu == p.HostUUID {
|
|
// remove element
|
|
hostUUIDs = append(hostUUIDs[:i], hostUUIDs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
enqueueFailForOp = ""
|
|
|
|
// All profiles will have bad contents
|
|
badContents := "bad-content: $FLEET_VAR_BOZO"
|
|
originalContents1 := contents1
|
|
originalContents2 := contents2
|
|
originalContents4 := contents4
|
|
originalContents5 := contents5
|
|
originalContents7 := contents7
|
|
contents1 = []byte(badContents)
|
|
contents2 = []byte(badContents)
|
|
contents4 = []byte(badContents)
|
|
contents5 = []byte(badContents)
|
|
contents7 = []byte(badContents)
|
|
t.Cleanup(func() {
|
|
contents1 = originalContents1
|
|
contents2 = originalContents2
|
|
contents4 = originalContents4
|
|
contents5 = originalContents5
|
|
contents7 = originalContents7
|
|
})
|
|
|
|
profilesToInstall, _, _ := ds.ListMDMAppleProfilesToInstallAndRemoveFunc(ctx)
|
|
hostUUIDs = make([]string, 0, len(profilesToInstall))
|
|
for _, p := range profilesToInstall {
|
|
// This host will error before this point - should not be updated by the variable failure
|
|
if p.HostUUID == hostUUID2 && (p.ProfileUUID == p5 || p.ProfileUUID == p7) {
|
|
continue
|
|
}
|
|
hostUUIDs = append(hostUUIDs, p.HostUUID)
|
|
}
|
|
|
|
err := ReconcileAppleProfiles(ctx, ds, cmdr, slog.New(slog.DiscardHandler))
|
|
require.NoError(t, err)
|
|
assert.Empty(t, hostUUIDs, "all host+profile combinations should be updated")
|
|
require.Equal(t, 5, failedCount, "number of profiles with bad content")
|
|
// checkAndReset(t, true, &ds.GetAllCertificateAuthoritiesFuncInvoked)
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallAndRemoveFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
checkAndReset(t, true, &ds.GetNanoMDMUserEnrollmentFuncInvoked)
|
|
// Check that individual updates were not done (bulk update should be done)
|
|
checkAndReset(t, false, &ds.UpdateOrDeleteHostMDMAppleProfileFuncInvoked)
|
|
})
|
|
}
|
|
|
|
func TestAppleMDMFileVaultEscrowFunctions(t *testing.T) {
|
|
svc := Service{}
|
|
|
|
err := svc.MDMAppleEnableFileVaultAndEscrow(context.Background(), ptr.Uint(1))
|
|
require.ErrorIs(t, fleet.ErrMissingLicense, err)
|
|
|
|
err = svc.MDMAppleDisableFileVaultAndEscrow(context.Background(), ptr.Uint(1))
|
|
require.ErrorIs(t, fleet.ErrMissingLicense, err)
|
|
}
|
|
|
|
func TestGenerateEnrollmentProfileMobileConfig(t *testing.T) {
|
|
// SCEP challenge should be escaped for XML
|
|
b, err := apple_mdm.GenerateEnrollmentProfileMobileconfig("foo", "https://example.com", "foo&bar", "topic")
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(b), "foo&bar")
|
|
}
|
|
|
|
func TestEnsureFleetdConfig(t *testing.T) {
|
|
testError := errors.New("test error")
|
|
testURL := "https://example.com"
|
|
testTeamName := "test-team"
|
|
logger := slog.New(slog.DiscardHandler)
|
|
mdmConfig := config.MDMConfig{
|
|
AppleSCEPCert: "./testdata/server.pem",
|
|
AppleSCEPKey: "./testdata/server.key",
|
|
}
|
|
signingCert, _, _, err := mdmConfig.AppleSCEP()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("no enroll secret found", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return []*fleet.EnrollSecret{}, nil
|
|
}
|
|
ds.BulkUpsertMDMAppleConfigProfilesFunc = func(ctx context.Context, ps []*fleet.MDMAppleConfigProfile) error {
|
|
require.Empty(t, ps)
|
|
return nil
|
|
}
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.NoError(t, err)
|
|
require.True(t, ds.BulkUpsertMDMAppleConfigProfilesFuncInvoked)
|
|
require.True(t, ds.AggregateEnrollSecretPerTeamFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
})
|
|
|
|
t.Run("all enroll secrets empty", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
secrets := []*fleet.EnrollSecret{
|
|
{Secret: "", TeamID: nil},
|
|
{Secret: "", TeamID: ptr.Uint(1)},
|
|
{Secret: "", TeamID: ptr.Uint(2)},
|
|
}
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return secrets, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.BulkUpsertMDMAppleConfigProfilesFunc = func(ctx context.Context, ps []*fleet.MDMAppleConfigProfile) error {
|
|
require.Empty(t, ps)
|
|
return nil
|
|
}
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.NoError(t, err)
|
|
require.True(t, ds.BulkUpsertMDMAppleConfigProfilesFuncInvoked)
|
|
require.True(t, ds.AggregateEnrollSecretPerTeamFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
})
|
|
|
|
t.Run("uses the enroll secret of each team if available", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
secrets := []*fleet.EnrollSecret{
|
|
{Secret: "global", TeamID: nil},
|
|
{Secret: "team-1", TeamID: ptr.Uint(1)},
|
|
{Secret: "team-2", TeamID: ptr.Uint(2)},
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.ServerSettings.ServerURL = testURL
|
|
appCfg.MDM.DeprecatedAppleBMDefaultTeam = testTeamName
|
|
return appCfg, nil
|
|
}
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return secrets, nil
|
|
}
|
|
ds.BulkUpsertMDMAppleConfigProfilesFunc = func(ctx context.Context, ps []*fleet.MDMAppleConfigProfile) error {
|
|
// fleetd + CA profiles
|
|
require.Len(t, ps, len(secrets)*2)
|
|
var fleetd, fleetCA []*fleet.MDMAppleConfigProfile
|
|
for _, p := range ps {
|
|
switch p.Identifier {
|
|
case mobileconfig.FleetdConfigPayloadIdentifier:
|
|
fleetd = append(fleetd, p)
|
|
case mobileconfig.FleetCARootConfigPayloadIdentifier:
|
|
fleetCA = append(fleetCA, p)
|
|
}
|
|
}
|
|
require.Len(t, fleetd, 3)
|
|
require.Len(t, fleetCA, 3)
|
|
|
|
for i, p := range fleetd {
|
|
require.Contains(t, string(p.Mobileconfig), testURL)
|
|
require.Contains(t, string(p.Mobileconfig), secrets[i].Secret)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.NoError(t, err)
|
|
require.True(t, ds.AggregateEnrollSecretPerTeamFuncInvoked)
|
|
require.True(t, ds.BulkUpsertMDMAppleConfigProfilesFuncInvoked)
|
|
})
|
|
|
|
t.Run("if the team doesn't have an enroll secret, fallback to no team", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
secrets := []*fleet.EnrollSecret{
|
|
{Secret: "global", TeamID: nil},
|
|
{Secret: "", TeamID: ptr.Uint(1)},
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.ServerSettings.ServerURL = testURL
|
|
appCfg.MDM.DeprecatedAppleBMDefaultTeam = testTeamName
|
|
return appCfg, nil
|
|
}
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return secrets, nil
|
|
}
|
|
ds.BulkUpsertMDMAppleConfigProfilesFunc = func(ctx context.Context, ps []*fleet.MDMAppleConfigProfile) error {
|
|
// fleetd + CA profiles
|
|
require.Len(t, ps, len(secrets)*2)
|
|
var fleetd, fleetCA []*fleet.MDMAppleConfigProfile
|
|
for _, p := range ps {
|
|
switch p.Identifier {
|
|
case mobileconfig.FleetdConfigPayloadIdentifier:
|
|
fleetd = append(fleetd, p)
|
|
case mobileconfig.FleetCARootConfigPayloadIdentifier:
|
|
fleetCA = append(fleetCA, p)
|
|
}
|
|
}
|
|
require.Len(t, fleetd, 2)
|
|
require.Len(t, fleetCA, 2)
|
|
|
|
for i, p := range fleetd {
|
|
require.Contains(t, string(p.Mobileconfig), testURL)
|
|
require.Contains(t, string(p.Mobileconfig), secrets[i].Secret)
|
|
}
|
|
return nil
|
|
}
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.NoError(t, err)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.AggregateEnrollSecretPerTeamFuncInvoked)
|
|
require.True(t, ds.BulkUpsertMDMAppleConfigProfilesFuncInvoked)
|
|
})
|
|
|
|
t.Run("returns an error if there's a problem retrieving AppConfig", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return nil, testError
|
|
}
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.ErrorIs(t, err, testError)
|
|
})
|
|
|
|
t.Run("returns an error if there's a problem retrieving secrets", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return nil, testError
|
|
}
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.ErrorIs(t, err, testError)
|
|
})
|
|
|
|
t.Run("returns an error if there's a problem upserting profiles", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
secrets := []*fleet.EnrollSecret{
|
|
{Secret: "global", TeamID: nil},
|
|
{Secret: "team-1", TeamID: ptr.Uint(1)},
|
|
}
|
|
ds.AggregateEnrollSecretPerTeamFunc = func(ctx context.Context) ([]*fleet.EnrollSecret, error) {
|
|
return secrets, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.BulkUpsertMDMAppleConfigProfilesFunc = func(ctx context.Context, p []*fleet.MDMAppleConfigProfile) error {
|
|
return testError
|
|
}
|
|
err := ensureFleetProfiles(ctx, ds, logger, signingCert.Certificate[0])
|
|
require.ErrorIs(t, err, testError)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.AggregateEnrollSecretPerTeamFuncInvoked)
|
|
require.True(t, ds.BulkUpsertMDMAppleConfigProfilesFuncInvoked)
|
|
})
|
|
}
|
|
|
|
func TestMDMAppleSetupAssistant(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
|
return j, nil
|
|
}
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
|
return &fleet.MDMAppleSetupAssistant{}, nil
|
|
}
|
|
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
|
|
return asst, nil
|
|
}
|
|
ds.DeleteMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) error {
|
|
return nil
|
|
}
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
return &fleet.Team{ID: id}, nil
|
|
}
|
|
ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
|
|
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
|
|
}
|
|
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
user *fleet.User
|
|
teamID *uint
|
|
shouldFailRead bool
|
|
shouldFailWrite bool
|
|
}{
|
|
{"no role no team", test.UserNoRoles, nil, true, true},
|
|
{"no role team", test.UserNoRoles, ptr.Uint(1), true, true},
|
|
{"global admin no team", test.UserAdmin, nil, false, false},
|
|
{"global admin team", test.UserAdmin, ptr.Uint(1), false, false},
|
|
{"global maintainer no team", test.UserMaintainer, nil, false, false},
|
|
{"global maintainer team", test.UserMaintainer, ptr.Uint(1), false, false},
|
|
{"global observer no team", test.UserObserver, nil, true, true},
|
|
{"global observer team", test.UserObserver, ptr.Uint(1), true, true},
|
|
{"global observer+ no team", test.UserObserverPlus, nil, true, true},
|
|
{"global observer+ team", test.UserObserverPlus, ptr.Uint(1), true, true},
|
|
{"global gitops no team", test.UserGitOps, nil, true, false},
|
|
{"global gitops team", test.UserGitOps, ptr.Uint(1), true, false},
|
|
{"team admin no team", test.UserTeamAdminTeam1, nil, true, true},
|
|
{"team admin team", test.UserTeamAdminTeam1, ptr.Uint(1), false, false},
|
|
{"team admin other team", test.UserTeamAdminTeam2, ptr.Uint(1), true, true},
|
|
{"team maintainer no team", test.UserTeamMaintainerTeam1, nil, true, true},
|
|
{"team maintainer team", test.UserTeamMaintainerTeam1, ptr.Uint(1), false, false},
|
|
{"team maintainer other team", test.UserTeamMaintainerTeam2, ptr.Uint(1), true, true},
|
|
{"team observer no team", test.UserTeamObserverTeam1, nil, true, true},
|
|
{"team observer team", test.UserTeamObserverTeam1, ptr.Uint(1), true, true},
|
|
{"team observer other team", test.UserTeamObserverTeam2, ptr.Uint(1), true, true},
|
|
{"team observer+ no team", test.UserTeamObserverPlusTeam1, nil, true, true},
|
|
{"team observer+ team", test.UserTeamObserverPlusTeam1, ptr.Uint(1), true, true},
|
|
{"team observer+ other team", test.UserTeamObserverPlusTeam2, ptr.Uint(1), true, true},
|
|
{"team gitops no team", test.UserTeamGitOpsTeam1, nil, true, true},
|
|
{"team gitops team", test.UserTeamGitOpsTeam1, ptr.Uint(1), true, false},
|
|
{"team gitops other team", test.UserTeamGitOpsTeam2, ptr.Uint(1), true, true},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// prepare the context with the user and license
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
|
|
_, err := svc.GetMDMAppleSetupAssistant(ctx, tt.teamID)
|
|
checkAuthErr(t, tt.shouldFailRead, err)
|
|
|
|
_, err = svc.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{
|
|
Name: "test",
|
|
Profile: json.RawMessage("{}"),
|
|
TeamID: tt.teamID,
|
|
})
|
|
checkAuthErr(t, tt.shouldFailWrite, err)
|
|
|
|
err = svc.DeleteMDMAppleSetupAssistant(ctx, tt.teamID)
|
|
checkAuthErr(t, tt.shouldFailWrite, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMApplePreassignEndpoints(t *testing.T) {
|
|
svc, ctx, _, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
checkAuthErr := func(t *testing.T, err error, shouldFailWithAuth bool) {
|
|
t.Helper()
|
|
|
|
if shouldFailWithAuth {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
user *fleet.User
|
|
shouldFail bool
|
|
}{
|
|
{"no role", test.UserNoRoles, true},
|
|
{"global admin", test.UserAdmin, false},
|
|
{"global maintainer", test.UserMaintainer, true},
|
|
{"global observer", test.UserObserver, true},
|
|
{"global observer+", test.UserObserverPlus, true},
|
|
{"global gitops", test.UserGitOps, false},
|
|
{"team admin", test.UserTeamAdminTeam1, true},
|
|
{"team maintainer", test.UserTeamMaintainerTeam1, true},
|
|
{"team observer", test.UserTeamObserverTeam1, true},
|
|
{"team observer+", test.UserTeamObserverPlusTeam1, true},
|
|
{"team gitops", test.UserTeamGitOpsTeam1, true},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// prepare the context with the user
|
|
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
|
|
|
err := svc.MDMApplePreassignProfile(ctx, fleet.MDMApplePreassignProfilePayload{
|
|
ExternalHostIdentifier: "test",
|
|
HostUUID: "test",
|
|
Profile: mobileconfigForTest("N1", "I1"),
|
|
})
|
|
checkAuthErr(t, err, tt.shouldFail)
|
|
|
|
err = svc.MDMAppleMatchPreassignment(ctx, "test")
|
|
checkAuthErr(t, err, tt.shouldFail)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper for creating scoped mobileconfigs. scope is optional and if set to nil is not included in
|
|
// the mobileconfig so that default behavior is used. Note that because Fleet enforces that all
|
|
// profiles sharing a given identifier have the same scope, it's a good idea to use a unique
|
|
// identifier in your test or perhaps one with the scope in its name
|
|
func scopedMobileconfigForTest(name, identifier string, scope *fleet.PayloadScope, vars ...string) []byte {
|
|
var varsStr strings.Builder
|
|
for i, v := range vars {
|
|
if !strings.HasPrefix(v, "FLEET_VAR_") {
|
|
v = "FLEET_VAR_" + v
|
|
}
|
|
varsStr.WriteString(fmt.Sprintf("<key>Var %d</key><string>$%s</string>", i, v))
|
|
}
|
|
scopeEntry := ""
|
|
if scope != nil {
|
|
scopeEntry = fmt.Sprintf(`\n<key>PayloadScope</key>\n<string>%s</string>`, string(*scope))
|
|
}
|
|
|
|
return []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array/>
|
|
<key>PayloadDisplayName</key>
|
|
<string>%s</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>%s</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>%s</string>%s
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
%s
|
|
</dict>
|
|
</plist>
|
|
`, name, identifier, uuid.New().String(), scopeEntry, varsStr.String()))
|
|
}
|
|
|
|
func mobileconfigForTest(name, identifier string, vars ...string) []byte {
|
|
return scopedMobileconfigForTest(name, identifier, nil, vars...)
|
|
}
|
|
|
|
func declBytesForTest(identifier string, payloadContent string) []byte {
|
|
tmpl := `{
|
|
"Type": "com.apple.configuration.decl%s",
|
|
"Identifier": "com.fleet.config%s",
|
|
"Payload": {
|
|
"ServiceType": "com.apple.service%s"
|
|
}
|
|
}`
|
|
|
|
declBytes := []byte(fmt.Sprintf(tmpl, identifier, identifier, payloadContent))
|
|
return declBytes
|
|
}
|
|
|
|
func mobileconfigForTestWithContent(outerName, outerIdentifier, innerIdentifier, innerType, innerName string) []byte {
|
|
if innerName == "" {
|
|
innerName = outerName + ".inner"
|
|
}
|
|
|
|
return []byte(fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>PayloadDisplayName</key>
|
|
<string>%s</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>%s</string>
|
|
<key>PayloadType</key>
|
|
<string>%s</string>
|
|
<key>PayloadUUID</key>
|
|
<string>3548D750-6357-4910-8DEA-D80ADCE2C787</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
<key>ShowRecoveryKey</key>
|
|
<false/>
|
|
</dict>
|
|
</array>
|
|
<key>PayloadDisplayName</key>
|
|
<string>%s</string>
|
|
<key>PayloadIdentifier</key>
|
|
<string>%s</string>
|
|
<key>PayloadType</key>
|
|
<string>Configuration</string>
|
|
<key>PayloadUUID</key>
|
|
<string>%s</string>
|
|
<key>PayloadVersion</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</plist>
|
|
`, innerName, innerIdentifier, innerType, outerName, outerIdentifier, uuid.New().String()))
|
|
}
|
|
|
|
func generateCertWithAPNsTopic() ([]byte, []byte, error) {
|
|
// generate a new private key
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// set up the OID for UID
|
|
oidUID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}
|
|
|
|
// set up a certificate template with the required UID in the Subject
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{
|
|
ExtraNames: []pkix.AttributeTypeAndValue{
|
|
{
|
|
Type: oidUID,
|
|
Value: "com.apple.mgmt.Example",
|
|
},
|
|
},
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
// create a self-signed certificate
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// encode to PEM
|
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
|
|
|
return certPEM, keyPEM, nil
|
|
}
|
|
|
|
func setupTest(t *testing.T) (context.Context, *slog.Logger, *mock.Store, *config.FleetConfig, *mdmmock.MDMAppleStore,
|
|
*apple_mdm.MDMAppleCommander,
|
|
) {
|
|
ctx := context.Background()
|
|
logger := slog.New(slog.DiscardHandler)
|
|
cfg := config.TestConfig()
|
|
ds := new(mock.Store)
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
stdlogfmt.New(),
|
|
)
|
|
mdmConfig := config.MDMConfig{
|
|
AppleSCEPCert: "./testdata/server.pem",
|
|
AppleSCEPKey: "./testdata/server.key",
|
|
}
|
|
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
|
require.NoError(t, err)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.MDM.EnabledAndConfigured = true
|
|
return appCfg, nil
|
|
}
|
|
|
|
_, pemCert, pemKey, err := mdmConfig.AppleSCEP()
|
|
require.NoError(t, err)
|
|
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetCACert: {Value: pemCert},
|
|
fleet.MDMAssetCAKey: {Value: pemKey},
|
|
fleet.MDMAssetAPNSKey: {Value: apnsKey},
|
|
fleet.MDMAssetAPNSCert: {Value: apnsCert},
|
|
}, nil
|
|
}
|
|
mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetCACert: {Value: pemCert},
|
|
fleet.MDMAssetCAKey: {Value: pemKey},
|
|
fleet.MDMAssetAPNSKey: {Value: apnsKey},
|
|
fleet.MDMAssetAPNSCert: {Value: apnsCert},
|
|
}, nil
|
|
}
|
|
|
|
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
|
|
|
return ctx, logger, ds, &cfg, mdmStorage, commander
|
|
}
|
|
|
|
func TestRenewSCEPCertificatesMDMConfigNotSet(t *testing.T) {
|
|
ctx, logger, ds, cfg, _, commander := setupTest(t)
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.MDM.EnabledAndConfigured = false
|
|
return appCfg, nil
|
|
}
|
|
err := RenewSCEPCertificates(ctx, logger, ds, cfg, commander)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestRenewSCEPCertificatesCommanderNil(t *testing.T) {
|
|
ctx, logger, ds, cfg, _, _ := setupTest(t)
|
|
err := RenewSCEPCertificates(ctx, logger, ds, cfg, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestRenewSCEPCertificatesBranches(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
customExpectations func(*testing.T, *mock.Store, *config.FleetConfig, *mdmmock.MDMAppleStore, *apple_mdm.MDMAppleCommander)
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "No Certs to Renew",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return nil, nil
|
|
}
|
|
},
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "GetHostCertAssociationsToExpire Errors",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return nil, errors.New("database error")
|
|
}
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "AppConfig Errors",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return nil, errors.New("app config error")
|
|
}
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "InstallProfile for hostsWithoutRefs",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
var wantCommandUUID string
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollReference: ""}}, nil
|
|
}
|
|
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
|
|
wantCommandUUID = cmd.CommandUUID
|
|
return map[string]error{}, nil
|
|
}
|
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
|
require.Len(t, assocs, 1)
|
|
require.Equal(t, "hostUUID1", assocs[0].HostUUID)
|
|
require.Equal(t, cmdUUID, wantCommandUUID)
|
|
return nil
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
require.True(t, appleStore.EnqueueCommandFuncInvoked)
|
|
require.True(t, ds.SetCommandForPendingSCEPRenewalFuncInvoked)
|
|
})
|
|
},
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "InstallProfile for hostsWithoutRefs fails",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollReference: ""}}, nil
|
|
}
|
|
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
return map[string]error{}, errors.New("foo")
|
|
}
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "InstallProfile for hostsWithRefs",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
var wantCommandUUID string
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID2", EnrollReference: "ref1"}}, nil
|
|
}
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
|
|
wantCommandUUID = cmd.CommandUUID
|
|
return map[string]error{}, nil
|
|
}
|
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
|
require.Len(t, assocs, 1)
|
|
require.Equal(t, "hostUUID2", assocs[0].HostUUID)
|
|
require.Equal(t, cmdUUID, wantCommandUUID)
|
|
return nil
|
|
}
|
|
t.Cleanup(func() {
|
|
require.True(t, appleStore.EnqueueCommandFuncInvoked)
|
|
require.True(t, ds.SetCommandForPendingSCEPRenewalFuncInvoked)
|
|
})
|
|
},
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "InstallProfile for hostsWithRefs fails",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollReference: "ref1"}}, nil
|
|
}
|
|
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
return map[string]error{}, errors.New("foo")
|
|
}
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "InstallProfile for userDeviceAssocs",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
wantCommandUUIDs := make(map[string]string)
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollmentType: "User Enrollment (Device)"}, {HostUUID: "hostUUID2", EnrollmentType: "User Enrollment (Device)"}}, nil
|
|
}
|
|
user1Email := "user1@example.com"
|
|
user2Email := "user2@example.com"
|
|
ds.GetMDMIdPAccountsByHostUUIDsFunc = func(ctx context.Context, hostUUIDs []string) (map[string]*fleet.MDMIdPAccount, error) {
|
|
require.Len(t, hostUUIDs, 2)
|
|
return map[string]*fleet.MDMIdPAccount{
|
|
"hostUUID2": {
|
|
UUID: "userUUID2",
|
|
Username: "user2",
|
|
Email: user2Email,
|
|
},
|
|
"hostUUID1": {
|
|
UUID: "userUUID1",
|
|
Username: "user1",
|
|
Email: user1Email,
|
|
},
|
|
}, nil
|
|
}
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
|
|
require.Equal(t, 1, len(id))
|
|
_, idAlreadyExists := wantCommandUUIDs[id[0]]
|
|
// Should only get one for each host
|
|
require.False(t, idAlreadyExists, "Command UUID for host %s already exists: %s", id[0], wantCommandUUIDs[id[0]])
|
|
wantCommandUUIDs[id[0]] = cmd.CommandUUID
|
|
|
|
// Make sure the user's email made it into the profile
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
switch id[0] {
|
|
case "hostUUID1":
|
|
require.True(t, bytes.Contains(fullCmd.Command.InstallProfile.Payload, []byte(user1Email)), "The profile for hostUUID 1 should contain the associated user email")
|
|
case "hostUUID2":
|
|
require.True(t, bytes.Contains(fullCmd.Command.InstallProfile.Payload, []byte(user2Email)), "The profile for hostUUID 2 should contain the associated user email")
|
|
default:
|
|
require.Fail(t, "Unexpected host ID for command: %s", id[0])
|
|
}
|
|
return map[string]error{}, nil
|
|
}
|
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
|
require.Len(t, assocs, 1)
|
|
require.Contains(t, []string{"hostUUID1", "hostUUID2"}, assocs[0].HostUUID)
|
|
require.Equal(t, cmdUUID, wantCommandUUIDs[assocs[0].HostUUID])
|
|
return nil
|
|
}
|
|
t.Cleanup(func() {
|
|
require.True(t, appleStore.EnqueueCommandFuncInvoked)
|
|
require.True(t, ds.SetCommandForPendingSCEPRenewalFuncInvoked)
|
|
})
|
|
},
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "InstallProfile for userDeviceAssocs does not return email for one device",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
wantCommandUUIDs := make(map[string]string)
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollmentType: "User Enrollment (Device)"}, {HostUUID: "hostUUID2", EnrollmentType: "User Enrollment (Device)"}}, nil
|
|
}
|
|
user1Email := "user1@example.com"
|
|
ds.GetMDMIdPAccountsByHostUUIDsFunc = func(ctx context.Context, hostUUIDs []string) (map[string]*fleet.MDMIdPAccount, error) {
|
|
require.Len(t, hostUUIDs, 2)
|
|
return map[string]*fleet.MDMIdPAccount{
|
|
"hostUUID1": {
|
|
UUID: "userUUID1",
|
|
Username: "user1",
|
|
Email: user1Email,
|
|
},
|
|
}, nil
|
|
}
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
|
|
require.Equal(t, 1, len(id))
|
|
_, idAlreadyExists := wantCommandUUIDs[id[0]]
|
|
// Should only get one for each host
|
|
require.False(t, idAlreadyExists, "Command UUID for host %s already exists: %s", id[0], wantCommandUUIDs[id[0]])
|
|
wantCommandUUIDs[id[0]] = cmd.CommandUUID
|
|
|
|
// Make sure the user's email made it into the profile if it was returned
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
switch id[0] {
|
|
// Only hostUUID1 has an email associated with it
|
|
// so we expect it to be present in the profile
|
|
case "hostUUID1":
|
|
require.True(t, bytes.Contains(fullCmd.Command.InstallProfile.Payload, []byte(user1Email)), "The profile for hostUUID 1 should contain the associated user email")
|
|
case "hostUUID2":
|
|
require.False(t, bytes.Contains(fullCmd.Command.InstallProfile.Payload, []byte("@example.com")), "The profile for hostUUID 2 should not contain any user email")
|
|
default:
|
|
require.Fail(t, "Unexpected host ID for command: %s", id[0])
|
|
}
|
|
return map[string]error{}, nil
|
|
}
|
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
|
require.Len(t, assocs, 1)
|
|
require.Contains(t, []string{"hostUUID1", "hostUUID2"}, assocs[0].HostUUID)
|
|
require.Equal(t, cmdUUID, wantCommandUUIDs[assocs[0].HostUUID])
|
|
return nil
|
|
}
|
|
t.Cleanup(func() {
|
|
require.True(t, appleStore.EnqueueCommandFuncInvoked)
|
|
require.True(t, ds.SetCommandForPendingSCEPRenewalFuncInvoked)
|
|
})
|
|
},
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "InstallProfile for userDeviceAssocs fails",
|
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mdmmock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollmentType: "User Enrollment (Device)"}, {HostUUID: "hostUUID2", EnrollmentType: "User Enrollment (Device)"}}, nil
|
|
}
|
|
user1Email := "user1@example.com"
|
|
user2Email := "user2@example.com"
|
|
ds.GetMDMIdPAccountsByHostUUIDsFunc = func(ctx context.Context, hostUUIDs []string) (map[string]*fleet.MDMIdPAccount, error) {
|
|
require.Len(t, hostUUIDs, 2)
|
|
return map[string]*fleet.MDMIdPAccount{
|
|
"hostUUID2": {
|
|
UUID: "userUUID2",
|
|
Username: "user2",
|
|
Email: user2Email,
|
|
},
|
|
"hostUUID1": {
|
|
UUID: "userUUID1",
|
|
Username: "user1",
|
|
Email: user1Email,
|
|
},
|
|
}, nil
|
|
}
|
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error,
|
|
error,
|
|
) {
|
|
return map[string]error{}, errors.New("foo")
|
|
}
|
|
},
|
|
expectedError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, logger, ds, cfg, appleStorage, commander := setupTest(t)
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
appCfg := &fleet.AppConfig{}
|
|
appCfg.OrgInfo.OrgName = "fl33t"
|
|
appCfg.ServerSettings.ServerURL = "https://foo.example.com"
|
|
appCfg.MDM.EnabledAndConfigured = true
|
|
return appCfg, nil
|
|
}
|
|
|
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
|
return []fleet.SCEPIdentityAssociation{}, nil
|
|
}
|
|
|
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
|
return nil
|
|
}
|
|
|
|
appleStorage.RetrievePushInfoFunc = func(ctx context.Context, targets []string) (map[string]*mdm.Push, error) {
|
|
pushes := make(map[string]*mdm.Push, len(targets))
|
|
for _, uuid := range targets {
|
|
pushes[uuid] = &mdm.Push{
|
|
PushMagic: "magic" + uuid,
|
|
Token: []byte("token" + uuid),
|
|
Topic: "topic" + uuid,
|
|
}
|
|
}
|
|
|
|
return pushes, nil
|
|
}
|
|
|
|
appleStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
|
|
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
|
require.NoError(t, err)
|
|
cert, err := tls.X509KeyPair(apnsCert, apnsKey)
|
|
return &cert, "", err
|
|
}
|
|
|
|
tc.customExpectations(t, ds, cfg, appleStorage, commander)
|
|
|
|
err := RenewSCEPCertificates(ctx, logger, ds, cfg, commander)
|
|
if tc.expectedError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
|
|
ctx := context.Background()
|
|
hostID := uint(42)
|
|
hostUUID := "ABC-DEF-GHI"
|
|
commandUUID := fleet.RefetchDeviceCommandUUIDPrefix + "UUID"
|
|
lostModeCommandUUID := uuid.NewString()
|
|
|
|
ds := new(mock.Store)
|
|
svc := MDMAppleCheckinAndCommandService{ds: ds, logger: slog.New(slog.DiscardHandler)}
|
|
|
|
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
|
return &fleet.Host{
|
|
ID: hostID,
|
|
UUID: hostUUID,
|
|
MDM: fleet.MDMHostData{
|
|
EnrollmentStatus: ptr.String("Pending"), // We check it in as a new device, to trigger lost mode flow
|
|
},
|
|
}, nil
|
|
}
|
|
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
|
require.Equal(t, "Work iPad", host.ComputerName)
|
|
require.Equal(t, "Work iPad", host.Hostname)
|
|
require.Equal(t, "iPadOS 17.5.1", host.OSVersion)
|
|
require.Equal(t, "ff:ff:ff:ff:ff:ff", host.PrimaryMac)
|
|
require.Equal(t, "iPad13,18", host.HardwareModel)
|
|
require.WithinDuration(t, time.Now(), host.DetailUpdatedAt, 1*time.Minute)
|
|
require.WithinDuration(t, time.Now(), host.LabelUpdatedAt, 1*time.Minute)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateHostDisksSpaceFunc = func(ctx context.Context, incomingHostID uint, gigsAvailable, percentAvailable, gigsTotal float64, gigsAll *float64) error {
|
|
require.Equal(t, hostID, incomingHostID)
|
|
require.NotZero(t, 51, int64(gigsAvailable))
|
|
require.NotZero(t, 79, int64(percentAvailable))
|
|
require.NotZero(t, 64, int64(gigsTotal))
|
|
return nil
|
|
}
|
|
ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, incomingHostID uint, hostOS fleet.OperatingSystem) error {
|
|
require.Equal(t, hostID, incomingHostID)
|
|
require.Equal(t, "iPadOS", hostOS.Name)
|
|
require.Equal(t, "17.5.1", hostOS.Version)
|
|
require.Equal(t, "ipados", hostOS.Platform)
|
|
return nil
|
|
}
|
|
ds.RemoveHostMDMCommandFunc = func(ctx context.Context, command fleet.HostMDMCommand) error {
|
|
assert.Equal(t, hostID, command.HostID)
|
|
assert.Equal(t, fleet.RefetchDeviceCommandUUIDPrefix, command.CommandType)
|
|
return nil
|
|
}
|
|
ds.UpdateMDMDataFunc = func(ctx context.Context, incomingHostID uint, enrolled bool) error {
|
|
require.Equal(t, hostID, incomingHostID)
|
|
return nil
|
|
}
|
|
ds.GetLatestAppleMDMCommandOfTypeFunc = func(ctx context.Context, incomingHostUUID, commandType string) (*fleet.MDMCommand, error) {
|
|
require.Equal(t, hostUUID, incomingHostUUID)
|
|
require.Equal(t, "EnableLostMode", commandType)
|
|
return &fleet.MDMCommand{
|
|
CommandUUID: lostModeCommandUUID,
|
|
}, nil
|
|
}
|
|
ds.SetLockCommandForLostModeCheckinFunc = func(ctx context.Context, incomingHostUUID uint, commandUUID string) error {
|
|
require.Equal(t, hostID, incomingHostUUID)
|
|
require.Equal(t, lostModeCommandUUID, commandUUID)
|
|
return nil
|
|
}
|
|
|
|
_, err := svc.CommandAndReportResults(
|
|
&mdm.Request{Context: ctx},
|
|
&mdm.CommandResults{
|
|
Enrollment: mdm.Enrollment{UDID: hostUUID},
|
|
CommandUUID: commandUUID,
|
|
Raw: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CommandUUID</key>
|
|
<string>REFETCH-fd23f8ac-1c50-41c7-a5bb-f13633c9ea97</string>
|
|
<key>QueryResponses</key>
|
|
<dict>
|
|
<key>AvailableDeviceCapacity</key>
|
|
<real>51.260395520000003</real>
|
|
<key>DeviceCapacity</key>
|
|
<real>64</real>
|
|
<key>DeviceName</key>
|
|
<string>Work iPad</string>
|
|
<key>OSVersion</key>
|
|
<string>17.5.1</string>
|
|
<key>ProductName</key>
|
|
<string>iPad13,18</string>
|
|
<key>WiFiMAC</key>
|
|
<string>ff:ff:ff:ff:ff:ff</string>
|
|
<key>IsMDMLostModeEnabled</key>
|
|
<true />
|
|
</dict>
|
|
<key>Status</key>
|
|
<string>Acknowledged</string>
|
|
<key>UDID</key>
|
|
<string>FFFFFFFF-FFFFFFFFFFFFFFFF</string>
|
|
</dict>
|
|
</plist>`),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.UpdateHostFuncInvoked)
|
|
require.True(t, ds.HostByIdentifierFuncInvoked)
|
|
require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked)
|
|
require.True(t, ds.UpdateHostOperatingSystemFuncInvoked)
|
|
assert.True(t, ds.RemoveHostMDMCommandFuncInvoked)
|
|
require.True(t, ds.UpdateMDMDataFuncInvoked)
|
|
require.True(t, ds.GetLatestAppleMDMCommandOfTypeFuncInvoked)
|
|
require.True(t, ds.SetLockCommandForLostModeCheckinFuncInvoked)
|
|
|
|
_, err = svc.CommandAndReportResults(
|
|
&mdm.Request{Context: ctx},
|
|
&mdm.CommandResults{
|
|
Enrollment: mdm.Enrollment{UDID: hostUUID},
|
|
CommandUUID: commandUUID,
|
|
Raw: []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CommandUUID</key>
|
|
<string>REFETCH-fd23f8ac-1c50-41c7-a5bb-f13633c9ea97</string>
|
|
<key>QueryResponses</key>
|
|
<dict>
|
|
<key>AvailableDeviceCapacity</key>
|
|
<real>51.260395520000003</real>
|
|
<key>DeviceCapacity</key>
|
|
<real>64</real>
|
|
<key>DeviceName</key>
|
|
<string>Work iPad</string>
|
|
<key>OSVersion</key>
|
|
<string>17.5.1</string>
|
|
<key>ProductName</key>
|
|
<string>iPad13,18</string>
|
|
<key>WiFiMAC</key>
|
|
<string>ff:ff:ff:ff:ff:ff</string>
|
|
</dict>
|
|
<key>Status</key>
|
|
<string>Acknowledged</string>
|
|
<key>UDID</key>
|
|
<string>FFFFFFFF-FFFFFFFFFFFFFFFF</string>
|
|
</dict>
|
|
</plist>`),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestUnmarshalAppList(t *testing.T) {
|
|
ctx := context.Background()
|
|
noApps := []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CommandUUID</key>
|
|
<string>c05c1a68-4127-4fde-b0da-965cbd63f88f</string>
|
|
<key>InstalledApplicationList</key>
|
|
<array/>
|
|
<key>Status</key>
|
|
<string>Acknowledged</string>
|
|
<key>UDID</key>
|
|
<string>00008030-000E6D623CD2202E</string>
|
|
</dict>
|
|
</plist>`)
|
|
software, err := unmarshalAppList(ctx, noApps, "ipados_apps")
|
|
require.NoError(t, err)
|
|
assert.Empty(t, software)
|
|
|
|
apps := []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CommandUUID</key>
|
|
<string>21ed54fc-0e6d-4fe3-8c4f-feca0c548ce1</string>
|
|
<key>InstalledApplicationList</key>
|
|
<array>
|
|
<dict>
|
|
<key>Identifier</key>
|
|
<string>com.google.ios.youtube</string>
|
|
<key>Name</key>
|
|
<string>YouTube</string>
|
|
<key>ShortVersion</key>
|
|
<string>19.29.1</string>
|
|
</dict>
|
|
<dict>
|
|
<key>Identifier</key>
|
|
<string>com.evernote.iPhone.Evernote</string>
|
|
<key>Name</key>
|
|
<string>Evernote</string>
|
|
<key>Installing</key>
|
|
<false/>
|
|
<key>ShortVersion</key>
|
|
<string>10.98.0</string>
|
|
</dict>
|
|
<dict>
|
|
<key>Identifier</key>
|
|
<string>com.netflix.Netflix</string>
|
|
<key>Name</key>
|
|
<string>Netflix</string>
|
|
<key>ShortVersion</key>
|
|
<string>16.41.0</string>
|
|
</dict>
|
|
</array>
|
|
<key>Status</key>
|
|
<string>Acknowledged</string>
|
|
<key>UDID</key>
|
|
<string>00008101-001514810EA3A01E</string>
|
|
</dict>
|
|
</plist>`)
|
|
expectedSoftware := []fleet.Software{
|
|
{
|
|
Name: "YouTube",
|
|
Version: "19.29.1",
|
|
Source: "ios_apps",
|
|
BundleIdentifier: "com.google.ios.youtube",
|
|
},
|
|
{
|
|
Name: "Evernote",
|
|
Version: "10.98.0",
|
|
Source: "ios_apps",
|
|
BundleIdentifier: "com.evernote.iPhone.Evernote",
|
|
Installed: true,
|
|
},
|
|
{
|
|
Name: "Netflix",
|
|
Version: "16.41.0",
|
|
Source: "ios_apps",
|
|
BundleIdentifier: "com.netflix.Netflix",
|
|
},
|
|
}
|
|
software, err = unmarshalAppList(ctx, apps, "ios_apps")
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, expectedSoftware, software)
|
|
}
|
|
|
|
func TestShouldOSUpdateForDEPEnrollment(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
platform string
|
|
appleMachineInfo fleet.MDMAppleMachineInfo
|
|
appleOSUpdateSettings fleet.AppleOSUpdateSettings
|
|
returnedErr error
|
|
|
|
expectedResult bool
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "when settings not found",
|
|
returnedErr: newNotFoundError(),
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
name: "error getting settings",
|
|
returnedErr: errors.New("Whoops"),
|
|
expectedErr: errors.New("Whoops"),
|
|
},
|
|
{
|
|
name: "if platform is macOS and update_new_hosts not set",
|
|
platform: string(fleet.MacOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.1",
|
|
},
|
|
appleOSUpdateSettings: fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(false),
|
|
MinimumVersion: optjson.SetString("16.0.2"),
|
|
},
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
name: "if platform is macOS and both update_new_hosts and minimum_version are set and host is below the minimum version",
|
|
platform: string(fleet.MacOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.1",
|
|
},
|
|
appleOSUpdateSettings: fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("16.0.2"),
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
name: "if platform is macOS and both update_new_hosts and minimum_version are set and host is at the minimum version",
|
|
platform: string(fleet.MacOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.2",
|
|
},
|
|
appleOSUpdateSettings: fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("16.0.2"),
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
},
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
name: "if platform is macOS and update_new_hosts is set but minimum_version is not set",
|
|
platform: string(fleet.MacOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.1",
|
|
},
|
|
appleOSUpdateSettings: fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
name: "if platform is not macOS and min_version is not set",
|
|
platform: string(fleet.IPadOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.1",
|
|
},
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
name: "if platform is not macOS and min_version is set and host's version is greater than min required",
|
|
platform: string(fleet.IPadOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.3",
|
|
},
|
|
appleOSUpdateSettings: fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("16.0.2"),
|
|
UpdateNewHosts: optjson.SetBool(false),
|
|
},
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
name: "if platform is not macOS and min_version is set and host's version is less than min required",
|
|
platform: string(fleet.IPadOSPlatform),
|
|
appleMachineInfo: fleet.MDMAppleMachineInfo{
|
|
OSVersion: "16.0.1",
|
|
},
|
|
appleOSUpdateSettings: fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("16.0.2"),
|
|
UpdateNewHosts: optjson.SetBool(false),
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ds := new(mock.Store)
|
|
for _, tt := range testCases {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, hostSerial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return tt.platform, &tt.appleOSUpdateSettings, tt.returnedErr
|
|
}
|
|
|
|
svc := &Service{ds: ds, logger: slog.New(slog.DiscardHandler)}
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := svc.shouldOSUpdateForDEPEnrollment(ctx, tt.appleMachineInfo)
|
|
require.Equal(t, tt.expectedResult, result)
|
|
require.Equal(t, tt.expectedErr, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) {
|
|
svc, ctx, ds, _ := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
|
|
gdmf := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
// load the test data from the file
|
|
b, err := os.ReadFile("../mdm/apple/gdmf/testdata/gdmf.json")
|
|
require.NoError(t, err)
|
|
_, err = w.Write(b)
|
|
require.NoError(t, err)
|
|
}))
|
|
defer gdmf.Close()
|
|
dev_mode.SetOverride("FLEET_DEV_GDMF_URL", gdmf.URL, t)
|
|
|
|
latestMacOSVersion := "14.6.1"
|
|
latestMacOSBuild := "23G93"
|
|
|
|
latestIOSVersion := "17.6.1"
|
|
latestIOSBuild := "21G93"
|
|
|
|
testCases := []struct {
|
|
name string
|
|
machineInfo *fleet.MDMAppleMachineInfo
|
|
updateRequired *fleet.MDMAppleSoftwareUpdateRequiredDetails
|
|
err string
|
|
}{
|
|
{
|
|
name: "OS version is greater than latest",
|
|
machineInfo: &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: true,
|
|
Product: "Mac15,7",
|
|
OSVersion: "14.6.2",
|
|
SupplementalBuildVersion: "IRRELEVANT",
|
|
SoftwareUpdateDeviceID: "J516sAP",
|
|
},
|
|
updateRequired: nil,
|
|
},
|
|
{
|
|
name: "OS version is equal to latest",
|
|
machineInfo: &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: true,
|
|
Product: "Mac15,7",
|
|
OSVersion: latestMacOSVersion,
|
|
SupplementalBuildVersion: "IRRELEVANT",
|
|
SoftwareUpdateDeviceID: "J516sAP",
|
|
},
|
|
updateRequired: nil,
|
|
},
|
|
{
|
|
name: "OS version is less than latest",
|
|
machineInfo: &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: true,
|
|
Product: "Mac15,7",
|
|
OSVersion: "14.4",
|
|
SupplementalBuildVersion: "IRRELEVANT",
|
|
SoftwareUpdateDeviceID: "J516sAP",
|
|
},
|
|
updateRequired: &fleet.MDMAppleSoftwareUpdateRequiredDetails{
|
|
OSVersion: latestMacOSVersion,
|
|
BuildVersion: latestMacOSBuild,
|
|
},
|
|
},
|
|
{
|
|
name: "OS version is less than latest but MDM cannot request software update",
|
|
machineInfo: &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: false,
|
|
Product: "Mac15,7",
|
|
OSVersion: "14.4",
|
|
SupplementalBuildVersion: "IRRELEVANT",
|
|
SoftwareUpdateDeviceID: "J516sAP",
|
|
},
|
|
updateRequired: nil,
|
|
},
|
|
{
|
|
name: "no match for software update device ID",
|
|
machineInfo: &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: true,
|
|
Product: "Mac15,7",
|
|
OSVersion: "14.4",
|
|
SupplementalBuildVersion: "IRRELEVANT",
|
|
SoftwareUpdateDeviceID: "INVALID",
|
|
},
|
|
updateRequired: nil,
|
|
err: "", // no error, allow enrollment to proceed without software update
|
|
},
|
|
{
|
|
name: "no machine info",
|
|
machineInfo: nil,
|
|
updateRequired: nil,
|
|
err: "", // no error, allow enrollment to proceed without software update
|
|
},
|
|
{
|
|
name: "cannot parse OS version",
|
|
machineInfo: &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: true,
|
|
Product: "Mac15,7",
|
|
OSVersion: "INVALID",
|
|
SupplementalBuildVersion: "IRRELEVANT",
|
|
SoftwareUpdateDeviceID: "J516sAP",
|
|
},
|
|
updateRequired: nil,
|
|
err: "", // no error, allow enrollment to proceed without software update
|
|
},
|
|
}
|
|
|
|
// FIXME: When we have more time, this whole test is overdue for a refactor because a bunch of jank
|
|
// came with the update new hosts settings for macOS that made the test cases more dependent on
|
|
// subtle differences in the machine info for macOS vs non-macOS platforms and made the setup
|
|
// more complex and harder to reason about. For now, we can get away with some nested subtests
|
|
// to reuse the test cases for both macOS and non-macOS platforms, but ideally we would refactor
|
|
// the function under test to separate out the platform-specific logic so that we can have
|
|
// clearer and more focused tests for each platform without needing to have a bunch of
|
|
// conditional logic in the test itself.
|
|
for _, tt := range testCases {
|
|
// Non-macOS platforms
|
|
for _, platform := range []string{"ios", "ipados"} {
|
|
|
|
if tt.name == "no match for software update device ID" {
|
|
// skip this test case for non-macOS platforms since SUDeviceID is really only relevant for macOS updates
|
|
continue
|
|
}
|
|
|
|
t.Run(fmt.Sprintf("%s: %s", platform, tt.name), func(t *testing.T) {
|
|
// switch up the machine info to match the platform because test cases were
|
|
// originally written with macOS in mind
|
|
var product, osVersion, suDeviceID string
|
|
var mi *fleet.MDMAppleMachineInfo
|
|
if tt.machineInfo != nil {
|
|
osVersion = strings.Replace(tt.machineInfo.OSVersion, "14", "17", 1)
|
|
if platform == "ios" {
|
|
product = "iPhone16,2"
|
|
suDeviceID = strings.Replace(tt.machineInfo.SoftwareUpdateDeviceID, "J516sAP", "iPhone", 1)
|
|
} else {
|
|
product = "iPad14,11"
|
|
suDeviceID = strings.Replace(tt.machineInfo.SoftwareUpdateDeviceID, "J516sAP", "iPad", 1)
|
|
}
|
|
|
|
mi = &fleet.MDMAppleMachineInfo{
|
|
MDMCanRequestSoftwareUpdate: tt.machineInfo.MDMCanRequestSoftwareUpdate,
|
|
Product: product,
|
|
OSVersion: osVersion,
|
|
SupplementalBuildVersion: tt.machineInfo.SupplementalBuildVersion,
|
|
SoftwareUpdateDeviceID: suDeviceID,
|
|
}
|
|
}
|
|
// same for update required details
|
|
var details *fleet.MDMAppleSoftwareUpdateRequiredDetails
|
|
if tt.updateRequired != nil {
|
|
details = &fleet.MDMAppleSoftwareUpdateRequiredDetails{
|
|
OSVersion: latestIOSVersion,
|
|
BuildVersion: latestIOSBuild,
|
|
}
|
|
}
|
|
|
|
t.Run("settings minimum equal to latest", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, &fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString(latestIOSVersion),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *details,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
})
|
|
|
|
t.Run("settings minimum below latest", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, &fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("17.5"),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *details,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
})
|
|
|
|
t.Run("settings minimum above latest", func(t *testing.T) {
|
|
// edge case, but in practice it would get treated as if minimum was equal to latest
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, &fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("17.7"),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *details,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
})
|
|
|
|
t.Run("device above settings minimum", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, &fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("17.1"),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Nil(t, sur)
|
|
})
|
|
|
|
t.Run("minimum not set", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, &fleet.AppleOSUpdateSettings{}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
require.NoError(t, err)
|
|
require.Nil(t, sur)
|
|
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, &fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString(""),
|
|
}, nil
|
|
}
|
|
sur, err = svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
require.NoError(t, err)
|
|
require.Nil(t, sur)
|
|
})
|
|
|
|
t.Run("minimum not found", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return platform, nil, ¬FoundError{}
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, mi)
|
|
require.NoError(t, err)
|
|
require.Nil(t, sur)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(fmt.Sprintf("%s for macOS", tt.name), func(t *testing.T) {
|
|
t.Run("when UpdateNewHosts is not set should never update", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "darwin", &fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(false),
|
|
MinimumVersion: optjson.SetString(latestMacOSVersion),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
require.NoError(t, err)
|
|
require.Nil(t, sur)
|
|
})
|
|
|
|
t.Run("when UpdateNewHosts is set and minimum is not set", func(t *testing.T) {
|
|
// test with min version not present
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "darwin", &fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
require.NoError(t, err)
|
|
|
|
// min version is not important for determining whether an update is required so the logic is based on
|
|
// the installed version only
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *tt.updateRequired,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
|
|
// test again with min version explicitly set to empty string
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "darwin", &fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
MinimumVersion: optjson.SetString(""),
|
|
}, nil
|
|
}
|
|
sur, err = svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
require.NoError(t, err)
|
|
|
|
// Ditto previous comment
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *tt.updateRequired,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
})
|
|
|
|
t.Run("when apple OS settings not found", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "darwin", nil, ¬FoundError{}
|
|
}
|
|
// never block enrollment when settings are not found
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
require.NoError(t, err)
|
|
require.Nil(t, sur)
|
|
})
|
|
|
|
t.Run("when UpdateNewHosts is set and required minimum is equal to latest", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "darwin", &fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
MinimumVersion: optjson.SetString(latestMacOSVersion),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *tt.updateRequired,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
})
|
|
|
|
t.Run("when UpdateNewHosts is set and required minimum is less than latest", func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "darwin", &fleet.AppleOSUpdateSettings{
|
|
UpdateNewHosts: optjson.SetBool(true),
|
|
MinimumVersion: optjson.SetString("14.5"),
|
|
}, nil
|
|
}
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
if tt.updateRequired != nil {
|
|
require.Equal(t, &fleet.MDMAppleSoftwareUpdateRequired{
|
|
Code: fleet.MDMAppleSoftwareUpdateRequiredCode,
|
|
Details: *tt.updateRequired,
|
|
}, sur)
|
|
} else {
|
|
require.Nil(t, sur)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
t.Run("gdmf server is down", func(t *testing.T) {
|
|
gdmf.Close()
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds.GetMDMAppleOSUpdatesSettingsByHostSerialFunc = func(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
|
|
return "macos", &fleet.AppleOSUpdateSettings{MinimumVersion: optjson.SetString(latestMacOSVersion), UpdateNewHosts: optjson.SetBool(true)}, nil
|
|
}
|
|
|
|
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, tt.machineInfo)
|
|
if tt.err != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.err) // still can get errors parsing the versions from the device info header or config settings
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Nil(t, sur) // if gdmf server is down, we don't enforce os updates for DEP
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateConfigProfileFleetVariablesLicense(t *testing.T) {
|
|
t.Parallel()
|
|
profileWithVars := `<?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>PayloadDescription</key>
|
|
<string>Test profile with Fleet variable</string>
|
|
<key>PayloadDisplayName</key>
|
|
<string>Test Profile</string>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>ComputerName</key>
|
|
<string>$FLEET_VAR_HOST_END_USER_EMAIL_IDP</string>
|
|
</dict>
|
|
</array>
|
|
</dict>
|
|
</plist>`
|
|
|
|
// Test with free license
|
|
freeLic := &fleet.LicenseInfo{Tier: fleet.TierFree}
|
|
_, err := validateConfigProfileFleetVariables(profileWithVars, freeLic, nil)
|
|
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
|
|
|
// Test with premium license
|
|
premiumLic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
|
vars, err := validateConfigProfileFleetVariables(profileWithVars, premiumLic, &fleet.GroupedCertificateAuthorities{})
|
|
require.NoError(t, err)
|
|
require.Contains(t, vars, "HOST_END_USER_EMAIL_IDP")
|
|
|
|
// Test profile without variables (should work with free license)
|
|
profileNoVars := `<?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>PayloadDescription</key>
|
|
<string>Test profile without Fleet variables</string>
|
|
<key>PayloadDisplayName</key>
|
|
<string>Test Profile</string>
|
|
<key>PayloadContent</key>
|
|
<array>
|
|
<dict>
|
|
<key>ComputerName</key>
|
|
<string>StaticValue</string>
|
|
</dict>
|
|
</array>
|
|
</dict>
|
|
</plist>`
|
|
vars, err = validateConfigProfileFleetVariables(profileNoVars, freeLic, &fleet.GroupedCertificateAuthorities{})
|
|
require.NoError(t, err)
|
|
require.Empty(t, vars)
|
|
}
|
|
|
|
func TestValidateConfigProfileFleetVariables(t *testing.T) {
|
|
t.Parallel()
|
|
groupedCAs := &fleet.GroupedCertificateAuthorities{
|
|
DigiCert: []fleet.DigiCertCA{
|
|
newMockDigicertCA("https://example.com", "caName"),
|
|
newMockDigicertCA("https://example.com", "caName2"),
|
|
},
|
|
CustomScepProxy: []fleet.CustomSCEPProxyCA{
|
|
newMockCustomSCEPProxyCA("https://example.com", "scepName"),
|
|
newMockCustomSCEPProxyCA("https://example.com", "scepName2"),
|
|
},
|
|
Smallstep: []fleet.SmallstepSCEPProxyCA{
|
|
newMockSmallstepSCEPProxyCA("https://example.com", "https://example.com/challenge", "smallstepName"),
|
|
newMockSmallstepSCEPProxyCA("https://example.com", "https://example.com/challenge", "smallstepName2"),
|
|
},
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
profile string
|
|
errMsg string
|
|
vars []string
|
|
}{
|
|
{
|
|
name: "DigiCert profile is not pkcs12",
|
|
profile: digiCertForValidation("$FLEET_VAR_DIGICERT_PASSWORD_caName", "$FLEET_VAR_DIGICERT_DATA_caName", "Name",
|
|
"com.apple.security.pkcs13"),
|
|
errMsg: "Variables $FLEET_VAR_DIGICERT_PASSWORD_caName and $FLEET_VAR_DIGICERT_DATA_caName can only be included in the 'com.apple.security.pkcs12' payload",
|
|
},
|
|
{
|
|
name: "DigiCert password is not a fleet variable",
|
|
profile: digiCertForValidation("x$FLEET_VAR_DIGICERT_PASSWORD_caName", "${FLEET_VAR_DIGICERT_DATA_caName}", "Name",
|
|
"com.apple.security.pkcs12"),
|
|
errMsg: "included in the 'com.apple.security.pkcs12' payload under Password and PayloadContent, respectively",
|
|
},
|
|
{
|
|
name: "DigiCert data is not a fleet variable",
|
|
profile: digiCertForValidation("${FLEET_VAR_DIGICERT_PASSWORD_caName}", "x${FLEET_VAR_DIGICERT_DATA_caName}", "Name",
|
|
"com.apple.security.pkcs12"),
|
|
errMsg: "Failed to parse PKCS12 payload with Fleet variables",
|
|
},
|
|
{
|
|
name: "DigiCert happy path",
|
|
profile: digiCertForValidation("${FLEET_VAR_DIGICERT_PASSWORD_caName}", "${FLEET_VAR_DIGICERT_DATA_caName}", "Name",
|
|
"com.apple.security.pkcs12"),
|
|
errMsg: "",
|
|
vars: []string{"DIGICERT_PASSWORD_caName", "DIGICERT_DATA_caName"},
|
|
},
|
|
{
|
|
name: "DigiCert 2 profiles with swapped variables",
|
|
profile: digiCertForValidation2("${FLEET_VAR_DIGICERT_PASSWORD_caName}", "${FLEET_VAR_DIGICERT_DATA_caName2}",
|
|
"$FLEET_VAR_DIGICERT_PASSWORD_caName2", "$FLEET_VAR_DIGICERT_DATA_caName"),
|
|
errMsg: "CA name mismatch between $FLEET_VAR_DIGICERT_PASSWORD_caName",
|
|
},
|
|
{
|
|
name: "DigiCert 2 profiles happy path",
|
|
profile: digiCertForValidation2("${FLEET_VAR_DIGICERT_PASSWORD_caName}", "${FLEET_VAR_DIGICERT_DATA_caName}",
|
|
"$FLEET_VAR_DIGICERT_PASSWORD_caName2", "$FLEET_VAR_DIGICERT_DATA_caName2"),
|
|
errMsg: "",
|
|
vars: []string{"DIGICERT_PASSWORD_caName", "DIGICERT_DATA_caName", "DIGICERT_PASSWORD_caName2", "DIGICERT_DATA_caName2"},
|
|
},
|
|
{
|
|
name: "Custom SCEP renewal ID shows up in the wrong place",
|
|
profile: customSCEPForValidationWithoutRenewalID("$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName", "$FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName",
|
|
"$FLEET_VAR_SCEP_RENEWAL_ID",
|
|
"com.apple.security.scep"),
|
|
errMsg: "Variable $FLEET_VAR_SCEP_RENEWAL_ID must be in the SCEP certificate's organizational unit (OU).",
|
|
},
|
|
{
|
|
name: "Custom SCEP profile is not scep",
|
|
profile: customSCEPForValidation("$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName", "$FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName",
|
|
"Name", "com.apple.security.SCEP"),
|
|
errMsg: fleet.SCEPVariablesNotInSCEPPayloadErrMsg,
|
|
},
|
|
{
|
|
name: "Custom SCEP challenge is not a fleet variable",
|
|
profile: customSCEPForValidation("x$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "must be in the SCEP certificate's \"Challenge\" field",
|
|
},
|
|
{
|
|
name: "Custom SCEP url is not a fleet variable",
|
|
profile: customSCEPForValidation("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName}", "x${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "must be in the SCEP certificate's \"URL\" field",
|
|
},
|
|
{
|
|
name: "Custom SCEP happy path",
|
|
profile: customSCEPForValidation("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName}", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "",
|
|
vars: []string{"CUSTOM_SCEP_CHALLENGE_scepName", "CUSTOM_SCEP_PROXY_URL_scepName", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "Custom SCEP happy path with OU renewal ID",
|
|
profile: customSCEPWithOURenewalIDForValidation("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName}", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "",
|
|
vars: []string{"CUSTOM_SCEP_CHALLENGE_scepName", "CUSTOM_SCEP_PROXY_URL_scepName", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "Custom SCEP 2 profiles with swapped variables",
|
|
profile: customSCEPForValidation2("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName2}", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"$FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName", "$FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName2"),
|
|
errMsg: fleet.MultipleSCEPPayloadsErrMsg,
|
|
},
|
|
{
|
|
name: "Custom SCEP 2 valid profiles should error",
|
|
profile: customSCEPForValidation2("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName}", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"challenge", "http://example2.com"),
|
|
errMsg: fleet.MultipleSCEPPayloadsErrMsg,
|
|
},
|
|
{
|
|
name: "Custom SCEP and DigiCert profiles happy path",
|
|
profile: customSCEPDigiCertForValidation("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName}", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}"),
|
|
errMsg: "",
|
|
vars: []string{"DIGICERT_PASSWORD_caName", "DIGICERT_DATA_caName", "CUSTOM_SCEP_CHALLENGE_scepName", "CUSTOM_SCEP_PROXY_URL_scepName", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "Custom profile with IdP variables and unknown variable",
|
|
profile: customProfileForValidation("$FLEET_VAR_HOST_END_USER_IDP_NO_SUCH_VAR"),
|
|
errMsg: "Fleet variable $FLEET_VAR_HOST_END_USER_IDP_NO_SUCH_VAR is not supported in configuration profiles.",
|
|
},
|
|
{
|
|
name: "Custom profile with IdP variables happy path",
|
|
profile: customProfileForValidation("value"),
|
|
errMsg: "",
|
|
vars: []string{
|
|
"HOST_END_USER_IDP_USERNAME",
|
|
"HOST_END_USER_IDP_USERNAME_LOCAL_PART",
|
|
"HOST_END_USER_IDP_GROUPS",
|
|
"HOST_END_USER_IDP_DEPARTMENT",
|
|
},
|
|
},
|
|
{
|
|
name: "Custom SCEP and NDES 2 valid profiles should error",
|
|
profile: customSCEPForValidation2("${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_scepName}", "${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_scepName}",
|
|
"$FLEET_VAR_NDES_SCEP_CHALLENGE", "$FLEET_VAR_NDES_SCEP_PROXY_URL"),
|
|
errMsg: fleet.MultipleSCEPPayloadsErrMsg,
|
|
},
|
|
{
|
|
name: "NDES renewal ID shows up in the wrong place",
|
|
profile: customSCEPForValidationWithoutRenewalID("$FLEET_VAR_NDES_SCEP_CHALLENGE", "$FLEET_VAR_NDES_SCEP_PROXY_URL",
|
|
"$FLEET_VAR_SCEP_RENEWAL_ID",
|
|
"com.apple.security.scep"),
|
|
errMsg: "Variable $FLEET_VAR_SCEP_RENEWAL_ID must be in the SCEP certificate's organizational unit (OU).",
|
|
},
|
|
{
|
|
name: "NDES profile is not scep",
|
|
profile: customSCEPForValidation("$FLEET_VAR_NDES_SCEP_CHALLENGE", "$FLEET_VAR_NDES_SCEP_PROXY_URL",
|
|
"Name", "com.apple.security.SCEP"),
|
|
errMsg: fleet.SCEPVariablesNotInSCEPPayloadErrMsg,
|
|
},
|
|
{
|
|
name: "NDES challenge is not a fleet variable",
|
|
profile: customSCEPForValidation("x$FLEET_VAR_NDES_SCEP_CHALLENGE", "${FLEET_VAR_NDES_SCEP_PROXY_URL}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "Variable \"$FLEET_VAR_NDES_SCEP_CHALLENGE\" must be in the SCEP certificate's \"Challenge\" field.",
|
|
},
|
|
{
|
|
name: "NDES url is not a fleet variable",
|
|
profile: customSCEPForValidation("${FLEET_VAR_NDES_SCEP_CHALLENGE}", "x${FLEET_VAR_NDES_SCEP_PROXY_URL}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "Variable \"$FLEET_VAR_NDES_SCEP_PROXY_URL\" must be in the SCEP certificate's \"URL\" field.",
|
|
},
|
|
{
|
|
name: "SCEP renewal ID without other variables",
|
|
profile: customSCEPForValidation("challenge", "url",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: fleet.SCEPRenewalIDWithoutURLChallengeErrMsg,
|
|
},
|
|
{
|
|
name: "NDES happy path",
|
|
profile: customSCEPForValidation("${FLEET_VAR_NDES_SCEP_CHALLENGE}", "${FLEET_VAR_NDES_SCEP_PROXY_URL}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "",
|
|
vars: []string{"NDES_SCEP_CHALLENGE", "NDES_SCEP_PROXY_URL", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "NDES happy path with OU renewal ID",
|
|
profile: customSCEPWithOURenewalIDForValidation("${FLEET_VAR_NDES_SCEP_CHALLENGE}", "${FLEET_VAR_NDES_SCEP_PROXY_URL}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "",
|
|
vars: []string{"NDES_SCEP_CHALLENGE", "NDES_SCEP_PROXY_URL", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "NDES 2 valid profiles should error",
|
|
profile: customSCEPForValidation2("${FLEET_VAR_NDES_SCEP_CHALLENGE}", "${FLEET_VAR_NDES_SCEP_PROXY_URL}",
|
|
"challenge", "http://example2.com"),
|
|
errMsg: fleet.MultipleSCEPPayloadsErrMsg,
|
|
},
|
|
{
|
|
name: "NDES and DigiCert profiles happy path",
|
|
profile: customSCEPDigiCertForValidation("${FLEET_VAR_NDES_SCEP_CHALLENGE}", "${FLEET_VAR_NDES_SCEP_PROXY_URL}"),
|
|
errMsg: "",
|
|
vars: []string{
|
|
"DIGICERT_PASSWORD_caName", "DIGICERT_DATA_caName", "NDES_SCEP_CHALLENGE", "NDES_SCEP_PROXY_URL",
|
|
"SCEP_RENEWAL_ID",
|
|
},
|
|
},
|
|
{
|
|
name: "Smallstep profile is not scep",
|
|
profile: customSCEPForValidation("$FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName", "$FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName",
|
|
"Name", "com.apple.security.SCEP"),
|
|
errMsg: fleet.SCEPVariablesNotInSCEPPayloadErrMsg,
|
|
},
|
|
{
|
|
name: "Smallstep happy path",
|
|
profile: customSCEPForValidation("${FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName}", "${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "",
|
|
vars: []string{"SMALLSTEP_SCEP_CHALLENGE_smallstepName", "SMALLSTEP_SCEP_PROXY_URL_smallstepName", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "Smallstep happy path with OU renewal ID",
|
|
profile: customSCEPWithOURenewalIDForValidation("${FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName}", "${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "",
|
|
vars: []string{"SMALLSTEP_SCEP_CHALLENGE_smallstepName", "SMALLSTEP_SCEP_PROXY_URL_smallstepName", "SCEP_RENEWAL_ID"},
|
|
},
|
|
{
|
|
name: "Smallstep 2 profiles with swapped variables",
|
|
profile: customSCEPForValidation2("${FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName2}", "${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"$FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName", "$FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName2"),
|
|
errMsg: fleet.MultipleSCEPPayloadsErrMsg,
|
|
},
|
|
{
|
|
name: "Smallstep 2 valid profiles should error",
|
|
profile: customSCEPForValidation2("${FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName}", "${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"challenge", "http://example2.com"),
|
|
errMsg: fleet.MultipleSCEPPayloadsErrMsg,
|
|
},
|
|
{
|
|
name: "Smallstep renewal ID shows up in the wrong place",
|
|
profile: customSCEPForValidationWithoutRenewalID("$FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName", "$FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName",
|
|
"$FLEET_VAR_SCEP_RENEWAL_ID",
|
|
"com.apple.security.scep"),
|
|
errMsg: "Variable $FLEET_VAR_SCEP_RENEWAL_ID must be in the SCEP certificate's organizational unit (OU).",
|
|
},
|
|
{
|
|
name: "Smallstep renewal ID in both CN and OU",
|
|
profile: customSCEPWithOURenewalIDForValidation("${FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName}", "${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"Name $FLEET_VAR_SCEP_RENEWAL_ID", "com.apple.security.scep"),
|
|
errMsg: "Variable $FLEET_VAR_SCEP_RENEWAL_ID must be in the SCEP certificate's organizational unit (OU).",
|
|
},
|
|
{
|
|
name: "Smallstep challenge is not a fleet variable",
|
|
profile: customSCEPForValidation("x$FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName", "${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "Variable \"$FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName\" must be in the SCEP certificate's \"Challenge\" field.",
|
|
},
|
|
{
|
|
name: "Smallstep url is not a fleet variable",
|
|
profile: customSCEPForValidation("${FLEET_VAR_SMALLSTEP_SCEP_CHALLENGE_smallstepName}", "x${FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName}",
|
|
"Name", "com.apple.security.scep"),
|
|
errMsg: "Variable \"$FLEET_VAR_SMALLSTEP_SCEP_PROXY_URL_smallstepName\" must be in the SCEP certificate's \"URL\" field.",
|
|
},
|
|
{
|
|
name: "Custom profile with IdP full name var",
|
|
profile: string(scopedMobileconfigForTest(
|
|
"FullName Var",
|
|
"com.example.fullname",
|
|
nil,
|
|
"HOST_END_USER_IDP_FULL_NAME", // will be prefixed to $FLEET_VAR_ by helper
|
|
)),
|
|
errMsg: "",
|
|
vars: []string{"HOST_END_USER_IDP_FULL_NAME"},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Pass a premium license for testing (we're not testing license validation here)
|
|
premiumLic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
|
vars, err := validateConfigProfileFleetVariables(tc.profile, premiumLic, groupedCAs)
|
|
if tc.errMsg != "" {
|
|
assert.ErrorContains(t, err, tc.errMsg)
|
|
assert.Empty(t, vars)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, tc.vars, vars)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
//go:embed testdata/profiles/digicert-validation.mobileconfig
|
|
var digiCertValidationMobileconfig string
|
|
|
|
func digiCertForValidation(password, data, name, payloadType string) string {
|
|
return fmt.Sprintf(digiCertValidationMobileconfig, password, data, name, payloadType)
|
|
}
|
|
|
|
//go:embed testdata/profiles/digicert-validation2.mobileconfig
|
|
var digiCertValidation2Mobileconfig string
|
|
|
|
func digiCertForValidation2(password1, data1, password2, data2 string) string {
|
|
return fmt.Sprintf(digiCertValidation2Mobileconfig, password1, data1, password2, data2)
|
|
}
|
|
|
|
//go:embed testdata/profiles/custom-scep-validation.mobileconfig
|
|
var customSCEPValidationMobileconfig string
|
|
|
|
func customSCEPForValidation(challenge, url, name, payloadType string) string {
|
|
return fmt.Sprintf(customSCEPValidationMobileconfig, challenge, url, name, payloadType)
|
|
}
|
|
|
|
func customSCEPForValidationWithoutRenewalID(challenge, url, name, payloadType string) string {
|
|
configProfile := strings.ReplaceAll(customSCEPValidationMobileconfig, "$FLEET_VAR_SCEP_RENEWAL_ID", "")
|
|
return fmt.Sprintf(configProfile, challenge, url, name, payloadType)
|
|
}
|
|
|
|
//go:embed testdata/profiles/custom-scep-validation2.mobileconfig
|
|
var customSCEPValidation2Mobileconfig string
|
|
|
|
func customSCEPForValidation2(challenge1, url1, challenge2, url2 string) string {
|
|
return fmt.Sprintf(customSCEPValidation2Mobileconfig, challenge1, url1, challenge2, url2)
|
|
}
|
|
|
|
//go:embed testdata/profiles/custom-scep-validation-ourenewal.mobileconfig
|
|
var customSCEPValidationWithOURenewalIDMobileconfig string
|
|
|
|
func customSCEPWithOURenewalIDForValidation(challenge, url, name, payloadType string) string {
|
|
return fmt.Sprintf(customSCEPValidationWithOURenewalIDMobileconfig, challenge, url, name, payloadType)
|
|
}
|
|
|
|
//go:embed testdata/profiles/custom-scep-digicert-validation.mobileconfig
|
|
var customSCEPDigiCertValidationMobileconfig string
|
|
|
|
func customSCEPDigiCertForValidation(challenge, url string) string {
|
|
return fmt.Sprintf(customSCEPDigiCertValidationMobileconfig, challenge, url)
|
|
}
|
|
|
|
//go:embed testdata/profiles/custom-profile-validation.mobileconfig
|
|
var customProfileValidationMobileconfig string
|
|
|
|
func customProfileForValidation(value string) string {
|
|
return fmt.Sprintf(customProfileValidationMobileconfig, value)
|
|
}
|
|
|
|
func TestParseHHMM(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantHour int
|
|
wantMin int
|
|
wantErr bool
|
|
}{
|
|
{"00:00", 0, 0, false},
|
|
{"09:30", 9, 30, false},
|
|
{"23:59", 23, 59, false},
|
|
{"12:05", 12, 5, false},
|
|
{"invalid", 0, 0, true},
|
|
{"12", 0, 0, true},
|
|
{"12:60", 0, 0, true},
|
|
{"24:00", 0, 0, true},
|
|
{"-01:00", 0, 0, true},
|
|
{"12:xx", 0, 0, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
h, m, err := parseHHMM(tt.input)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("parseHHMM(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
|
}
|
|
if err == nil && (h != tt.wantHour || m != tt.wantMin) {
|
|
t.Fatalf("parseHHMM(%q) = %d:%d, want %02d:%02d", tt.input, h, m, tt.wantHour, tt.wantMin)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentLocalTimeInHostTimeZone(t *testing.T) {
|
|
// Save original and restore after test
|
|
originalMock := nowFunc
|
|
defer func() { nowFunc = originalMock }()
|
|
|
|
// Fix "now" to a known UTC time: 2026-01-02 14:30:00 UTC
|
|
fixedNow := time.Date(2026, 1, 2, 14, 30, 0, 0, time.UTC)
|
|
nowFunc = func() time.Time {
|
|
return fixedNow
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
timezone string
|
|
wantHour int
|
|
wantMin int
|
|
wantErr bool
|
|
}{
|
|
{"UTC", "UTC", 14, 30, false},
|
|
{"New York", "America/New_York", 9, 30, false}, // EST = UTC-5
|
|
{"Los Angeles", "America/Los_Angeles", 6, 30, false}, // PST = UTC-8
|
|
{"London", "Europe/London", 14, 30, false}, // GMT in winter
|
|
{"Tokyo", "Asia/Tokyo", 23, 30, false}, // JST = UTC+9
|
|
{"Invalid TZ", "Invalid/Timezone", 0, 0, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := getCurrentLocalTimeInHostTimeZone(context.Background(), tt.timezone)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("getCurrentLocalTimeInHostTimeZone(%q) error = %v, wantErr %v", tt.timezone, err, tt.wantErr)
|
|
}
|
|
if err == nil {
|
|
if got.Hour() != tt.wantHour || got.Minute() != tt.wantMin {
|
|
t.Fatalf("getCurrentLocalTimeInHostTimeZone(%q) = %02d:%02d, want %02d:%02d",
|
|
tt.timezone, got.Hour(), got.Minute(), tt.wantHour, tt.wantMin)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsTimezoneInWindow(t *testing.T) {
|
|
// Save original and restore after test
|
|
originalMock := nowFunc
|
|
defer func() { nowFunc = originalMock }()
|
|
|
|
// Helper to set a fixed UTC time, which will be converted to local time
|
|
setMockUTCNow := func(year, month, day, hour, min_ int) {
|
|
nowFunc = func() time.Time {
|
|
return time.Date(year, time.Month(month), day, hour, min_, 0, 0, time.UTC)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
mockUTCHour int
|
|
mockUTCMin int
|
|
timezone string
|
|
start string
|
|
end string
|
|
wantInWindow bool
|
|
wantErr bool
|
|
}{
|
|
// Normal window (no midnight cross)
|
|
{name: "normal inside", mockUTCHour: 14, mockUTCMin: 0, timezone: "America/New_York", start: "08:00", end: "17:00", wantInWindow: true, wantErr: false}, // 09:00 EST
|
|
{name: "normal before", mockUTCHour: 12, mockUTCMin: 0, timezone: "America/New_York", start: "09:00", end: "17:00", wantInWindow: false, wantErr: false}, // 07:00 EST
|
|
{name: "normal at start", mockUTCHour: 14, mockUTCMin: 0, timezone: "America/New_York", start: "09:00", end: "17:00", wantInWindow: true, wantErr: false},
|
|
{name: "normal at end", mockUTCHour: 22, mockUTCMin: 0, timezone: "America/New_York", start: "09:00", end: "17:00", wantInWindow: true, wantErr: false}, // 17:00 EST
|
|
|
|
// Midnight-crossing window
|
|
{name: "too early", mockUTCHour: 4, mockUTCMin: 0, timezone: "America/Los_Angeles", start: "22:00", end: "06:00", wantInWindow: false, wantErr: false}, // 20:00 PST
|
|
{name: "crossing late night", mockUTCHour: 7, mockUTCMin: 0, timezone: "America/Los_Angeles", start: "22:00", end: "06:00", wantInWindow: true, wantErr: false}, // 23:00 PST
|
|
{name: "crossing early morning", mockUTCHour: 12, mockUTCMin: 0, timezone: "America/Los_Angeles", start: "22:00", end: "06:00", wantInWindow: true, wantErr: false}, // 04:00 PST
|
|
{name: "crossing outside", mockUTCHour: 16, mockUTCMin: 0, timezone: "America/Los_Angeles", start: "22:00", end: "06:00", wantInWindow: false, wantErr: false}, // 08:00 PST
|
|
|
|
// Edge cases
|
|
{name: "full day", mockUTCHour: 12, mockUTCMin: 0, timezone: "UTC", start: "00:00", end: "23:59", wantInWindow: true, wantErr: false},
|
|
{name: "single minute", mockUTCHour: 10, mockUTCMin: 5, timezone: "UTC", start: "10:05", end: "10:05", wantInWindow: true, wantErr: false},
|
|
{name: "invalid start", mockUTCHour: 12, mockUTCMin: 0, timezone: "UTC", start: "25:00", end: "17:00", wantInWindow: false, wantErr: true},
|
|
{name: "invalid timezone", mockUTCHour: 12, mockUTCMin: 0, timezone: "Invalid/TZ", start: "09:00", end: "17:00", wantInWindow: false, wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setMockUTCNow(2026, 1, 2, tt.mockUTCHour, tt.mockUTCMin)
|
|
|
|
got, err := isTimezoneInWindow(context.Background(), tt.timezone, tt.start, tt.end)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("isTimezoneInWindow(%q, %s-%s) error = %v, wantErr %v", tt.timezone, tt.start, tt.end, err, tt.wantErr)
|
|
}
|
|
if got != tt.wantInWindow {
|
|
t.Fatalf("isTimezoneInWindow(%q, %s-%s) = %v, want %v", tt.timezone, tt.start, tt.end, got, tt.wantInWindow)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToValidSemVer(t *testing.T) {
|
|
testVersions := []struct {
|
|
rawVersion string
|
|
expectedVersion string
|
|
versionToSemverVersionExpectedToFail bool
|
|
}{
|
|
{
|
|
"25.48.0",
|
|
"25.48.0",
|
|
false,
|
|
},
|
|
{
|
|
" 353.0 ", // Meta Horizon like version.
|
|
"353.0",
|
|
false,
|
|
},
|
|
{
|
|
"18.14.0",
|
|
"18.14.0",
|
|
false,
|
|
},
|
|
{
|
|
"412.0.0",
|
|
"412.0.0",
|
|
false,
|
|
},
|
|
{
|
|
"00.001010.01",
|
|
"0.1010.1",
|
|
false,
|
|
},
|
|
{
|
|
"6.0.251229",
|
|
"6.0.251229",
|
|
false,
|
|
},
|
|
{
|
|
"4.2602.11600",
|
|
"4.2602.11600",
|
|
false,
|
|
},
|
|
{
|
|
"144.0.7559.53", // Google Chrome like version.
|
|
"144.0.7559-53",
|
|
false,
|
|
},
|
|
{
|
|
"144.0.7559.03", // Google Chrome like version, leading zeros.
|
|
"144.0.7559-3",
|
|
false,
|
|
},
|
|
{
|
|
"4.9999999999999999999999999.11600", // Not a valid semantic version, so we leave unchanged.
|
|
"4.9999999999999999999999999.11600",
|
|
true,
|
|
},
|
|
{
|
|
"04.0000099999999999999999999999990.011600", // Not a valid semantic version, but we clean it anyway.
|
|
"4.99999999999999999999999990.11600",
|
|
true,
|
|
},
|
|
{
|
|
"21.02.3", // YouTube like version.
|
|
"21.2.3",
|
|
false,
|
|
},
|
|
{
|
|
"21", // Just major version.
|
|
"21",
|
|
false,
|
|
},
|
|
{
|
|
"v2.3.4", // Remove leading v.
|
|
"2.3.4",
|
|
false,
|
|
},
|
|
{
|
|
"02.03.04-01",
|
|
"2.3.4-1",
|
|
false,
|
|
},
|
|
}
|
|
for _, tc := range testVersions {
|
|
cleanedVersion := toValidSemVer(tc.rawVersion)
|
|
require.Equal(t, tc.expectedVersion, cleanedVersion)
|
|
_, err := fleet.VersionToSemverVersion(cleanedVersion)
|
|
if !tc.versionToSemverVersionExpectedToFail {
|
|
require.NoError(t, err, tc.rawVersion)
|
|
} else {
|
|
require.Error(t, err, tc.rawVersion)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMDMTokenUpdateSCEPRenewal(t *testing.T) {
|
|
ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
|
ds := new(mock.Store)
|
|
mdmStorage := &mdmmock.MDMAppleStore{}
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
pusher := nanomdm_pushsvc.New(
|
|
mdmStorage,
|
|
mdmStorage,
|
|
pushFactory,
|
|
NewNanoMDMLogger(logger),
|
|
)
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
|
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
|
|
|
|
t.Run("awaiting configuration continues enrollment", func(t *testing.T) {
|
|
// When a host re-enrolls via DEP (AwaitingConfiguration=true) while a
|
|
// SCEP renewal is pending, the handler should clear SCEP refs and
|
|
// continue with the normal enrollment flow (not short-circuit).
|
|
|
|
var newActivityFuncInvoked bool
|
|
mdmLifecycle := mdmlifecycle.New(ds, logger, func(_ context.Context, _ *fleet.User, activity fleet.ActivityDetails) error {
|
|
newActivityFuncInvoked = true
|
|
_, ok := activity.(*fleet.ActivityTypeMDMEnrolled)
|
|
require.True(t, ok)
|
|
return nil
|
|
})
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
commander: cmdr,
|
|
logger: logger,
|
|
}
|
|
scepRenewalInProgress := true
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: true,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
SCEPRenewalInProgress: scepRenewalInProgress,
|
|
Platform: "darwin",
|
|
}, nil
|
|
}
|
|
ds.CleanSCEPRenewRefsFunc = func(ctx context.Context, hostUUID string) error {
|
|
require.Equal(t, uuid, hostUUID)
|
|
scepRenewalInProgress = false
|
|
return nil
|
|
}
|
|
ds.EnqueueSetupExperienceItemsFunc = func(ctx context.Context, hostPlatform, hostPlatformLike string, hostUUID string, teamID uint) (bool, error) {
|
|
require.Equal(t, "darwin", hostPlatformLike)
|
|
require.Equal(t, uuid, hostUUID)
|
|
require.Equal(t, wantTeamID, teamID)
|
|
return true, nil
|
|
}
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil
|
|
}
|
|
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
|
|
return &fleet.AppConfig{}, nil
|
|
}
|
|
ds.GetMDMIdPAccountByHostUUIDFunc = func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
|
|
return nil, nil
|
|
}
|
|
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
|
return j, nil
|
|
}
|
|
ds.MDMResetEnrollmentFunc = func(ctx context.Context, hostUUID string, scepRenewalInProgress bool) error {
|
|
return nil
|
|
}
|
|
|
|
err := svc.TokenUpdate(
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
AwaitingConfiguration: true,
|
|
Enrollment: mdm.Enrollment{
|
|
UDID: uuid,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.CleanSCEPRenewRefsFuncInvoked)
|
|
require.True(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
require.True(t, ds.NewJobFuncInvoked)
|
|
require.True(t, newActivityFuncInvoked)
|
|
require.True(t, ds.MDMResetEnrollmentFuncInvoked)
|
|
})
|
|
|
|
t.Run("not awaiting configuration short-circuits", func(t *testing.T) {
|
|
// When a SCEP renewal is in progress but the host is NOT awaiting
|
|
// configuration (normal renewal), the handler should clean SCEP refs
|
|
// and return early without enqueueing setup experience or lifecycle.
|
|
var newActivityFuncInvoked bool
|
|
mdmLifecycle := mdmlifecycle.New(ds, logger, func(_ context.Context, _ *fleet.User, _ fleet.ActivityDetails) error {
|
|
newActivityFuncInvoked = true
|
|
return nil
|
|
})
|
|
svc := MDMAppleCheckinAndCommandService{
|
|
ds: ds,
|
|
mdmLifecycle: mdmLifecycle,
|
|
commander: cmdr,
|
|
logger: logger,
|
|
}
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
return &fleet.HostMDMCheckinInfo{
|
|
HostID: 1337,
|
|
HardwareSerial: serial,
|
|
DisplayName: model,
|
|
InstalledFromDEP: true,
|
|
TeamID: wantTeamID,
|
|
DEPAssignedToFleet: true,
|
|
SCEPRenewalInProgress: true,
|
|
Platform: "darwin",
|
|
}, nil
|
|
}
|
|
ds.CleanSCEPRenewRefsFunc = func(ctx context.Context, hostUUID string) error {
|
|
require.Equal(t, uuid, hostUUID)
|
|
return nil
|
|
}
|
|
ds.CleanSCEPRenewRefsFuncInvoked = false
|
|
ds.EnqueueSetupExperienceItemsFuncInvoked = false
|
|
ds.NewJobFuncInvoked = false
|
|
|
|
err := svc.TokenUpdate(
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
&mdm.TokenUpdate{
|
|
TokenUpdateEnrollment: mdm.TokenUpdateEnrollment{
|
|
Enrollment: mdm.Enrollment{UDID: uuid},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.CleanSCEPRenewRefsFuncInvoked)
|
|
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
require.False(t, newActivityFuncInvoked)
|
|
})
|
|
}
|