2022-10-05 22:53:54 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2023-01-31 14:46:01 +00:00
|
|
|
"crypto/tls"
|
2023-02-17 19:26:51 +00:00
|
|
|
"encoding/base64"
|
2023-04-25 13:36:01 +00:00
|
|
|
"encoding/json"
|
2023-03-27 18:43:01 +00:00
|
|
|
"errors"
|
2023-01-23 23:05:24 +00:00
|
|
|
"fmt"
|
2022-10-05 22:53:54 +00:00
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2023-01-31 14:46:01 +00:00
|
|
|
"os"
|
2022-10-05 22:53:54 +00:00
|
|
|
"strings"
|
2023-01-23 23:05:24 +00:00
|
|
|
"sync/atomic"
|
2022-10-05 22:53:54 +00:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
2023-02-15 18:01:44 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
2023-01-23 23:05:24 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
2022-10-05 22:53:54 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2023-03-13 13:33:32 +00:00
|
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
2023-03-17 21:52:30 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
2022-10-05 22:53:54 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
2023-01-31 14:46:01 +00:00
|
|
|
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
|
|
|
nanomdm_mock "github.com/fleetdm/fleet/v4/server/mock/nanomdm"
|
2023-01-23 23:05:24 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
2022-10-05 22:53:54 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
2023-01-31 14:46:01 +00:00
|
|
|
kitlog "github.com/go-kit/kit/log"
|
2023-02-15 18:01:44 +00:00
|
|
|
"github.com/google/uuid"
|
2022-10-05 22:53:54 +00:00
|
|
|
nanodep_client "github.com/micromdm/nanodep/client"
|
|
|
|
"github.com/micromdm/nanomdm/mdm"
|
2023-01-31 14:46:01 +00:00
|
|
|
nanomdm_pushsvc "github.com/micromdm/nanomdm/push/service"
|
2022-10-05 22:53:54 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
2023-05-10 20:22:08 +00:00
|
|
|
func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Service, context.Context, *mock.Store) {
|
2022-10-05 22:53:54 +00:00
|
|
|
ds := new(mock.Store)
|
|
|
|
cfg := config.TestConfig()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
switch {
|
|
|
|
case strings.Contains(r.URL.Path, "/server/devices"):
|
2022-12-05 22:50:49 +00:00
|
|
|
_, err := w.Write([]byte("{}"))
|
|
|
|
require.NoError(t, err)
|
2022-10-05 22:53:54 +00:00
|
|
|
return
|
|
|
|
case strings.Contains(r.URL.Path, "/session"):
|
2022-12-05 22:50:49 +00:00
|
|
|
_, err := w.Write([]byte(`{"auth_session_token": "yoo"}`))
|
|
|
|
require.NoError(t, err)
|
2022-10-05 22:53:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}))
|
2023-01-23 23:05:24 +00:00
|
|
|
|
2023-01-31 14:46:01 +00:00
|
|
|
mdmStorage := &nanomdm_mock.Storage{}
|
|
|
|
depStorage := &nanodep_mock.Storage{}
|
|
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
|
|
pusher := nanomdm_pushsvc.New(
|
|
|
|
mdmStorage,
|
|
|
|
mdmStorage,
|
|
|
|
pushFactory,
|
|
|
|
NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)),
|
|
|
|
)
|
|
|
|
|
2023-01-23 23:05:24 +00:00
|
|
|
opts := &TestServerOpts{
|
2022-10-05 22:53:54 +00:00
|
|
|
FleetConfig: &cfg,
|
2023-01-31 14:46:01 +00:00
|
|
|
MDMStorage: mdmStorage,
|
|
|
|
DEPStorage: depStorage,
|
|
|
|
MDMPusher: pusher,
|
2023-05-10 20:22:08 +00:00
|
|
|
License: license,
|
2023-01-31 14:46:01 +00:00
|
|
|
}
|
|
|
|
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, opts)
|
|
|
|
|
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
|
|
|
return nil, nil
|
2023-01-23 23:05:24 +00:00
|
|
|
}
|
2023-01-31 14:46:01 +00:00
|
|
|
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
|
2023-01-23 23:05:24 +00:00
|
|
|
}
|
2023-01-31 14:46:01 +00:00
|
|
|
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
|
2023-01-23 23:05:24 +00:00
|
|
|
}
|
2023-01-31 14:46:01 +00:00
|
|
|
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
|
2023-01-23 23:05:24 +00:00
|
|
|
}
|
|
|
|
|
2022-10-05 22:53:54 +00:00
|
|
|
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",
|
|
|
|
},
|
2023-03-27 19:30:29 +00:00
|
|
|
MDM: fleet.MDM{
|
|
|
|
EnabledAndConfigured: true,
|
|
|
|
},
|
2022-10-05 22:53:54 +00:00
|
|
|
}, 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
|
|
|
|
}
|
2023-03-27 18:43:01 +00:00
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
|
|
|
return &fleet.NanoEnrollment{Enabled: false}, nil
|
2023-01-23 23:05:24 +00:00
|
|
|
}
|
2023-04-05 14:50:36 +00:00
|
|
|
ds.GetMDMAppleCommandRequestTypeFunc = func(ctx context.Context, commandUUID string) (string, error) {
|
|
|
|
return "", nil
|
|
|
|
}
|
2023-05-02 13:09:33 +00:00
|
|
|
ds.MDMAppleGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMAppleEULA, error) {
|
|
|
|
return &fleet.MDMAppleEULA{}, nil
|
|
|
|
}
|
|
|
|
ds.MDMAppleGetEULABytesFunc = func(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) {
|
|
|
|
return &fleet.MDMAppleEULA{}, nil
|
|
|
|
}
|
|
|
|
ds.MDMAppleInsertEULAFunc = func(ctx context.Context, eula *fleet.MDMAppleEULA) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ds.MDMAppleDeleteEULAFunc = func(ctx context.Context, token string) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-01-23 23:05:24 +00:00
|
|
|
|
2023-01-16 20:06:30 +00:00
|
|
|
return svc, ctx, ds
|
2022-10-05 22:53:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestAppleMDMAuthorization(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2022-10-05 22:53:54 +00:00
|
|
|
|
|
|
|
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) {
|
2022-11-15 14:08:05 +00:00
|
|
|
ctx := test.UserContext(ctx, user)
|
2023-05-09 17:00:18 +00:00
|
|
|
_, err := svc.UploadMDMAppleInstaller(ctx, "foo", 3, bytes.NewReader([]byte("foo")))
|
2022-10-05 22:53:54 +00:00
|
|
|
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)
|
|
|
|
_, err = svc.ListMDMAppleDEPDevices(ctx)
|
|
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
2023-05-02 13:09:33 +00:00
|
|
|
|
|
|
|
// check EULA routes
|
|
|
|
_, err = svc.MDMAppleGetEULAMetadata(ctx)
|
|
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
|
|
err = svc.MDMAppleCreateEULA(ctx, "eula.pdf", bytes.NewReader([]byte("%PDF-")))
|
|
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
|
|
|
err = svc.MDMAppleDeleteEULA(ctx, "foo")
|
|
|
|
checkAuthErr(t, err, shouldFailWithAuth)
|
2022-10-05 22:53:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Only global admins can access the endpoints.
|
|
|
|
testAuthdMethods(t, test.UserAdmin, false)
|
|
|
|
|
|
|
|
// All other users should not have access to the endpoints.
|
|
|
|
for _, user := range []*fleet.User{
|
|
|
|
test.UserNoRoles,
|
|
|
|
test.UserMaintainer,
|
|
|
|
test.UserObserver,
|
2023-04-05 18:23:49 +00:00
|
|
|
test.UserObserverPlus,
|
2022-10-05 22:53:54 +00:00
|
|
|
test.UserTeamAdminTeam1,
|
|
|
|
} {
|
|
|
|
testAuthdMethods(t, user, true)
|
|
|
|
}
|
|
|
|
// Token authenticated endpoints can be accessed by anyone.
|
2022-11-15 14:08:05 +00:00
|
|
|
ctx = test.UserContext(ctx, test.UserNoRoles)
|
2022-10-05 22:53:54 +00:00
|
|
|
_, err := svc.GetMDMAppleInstallerByToken(ctx, "foo")
|
|
|
|
require.NoError(t, err)
|
2023-05-18 15:50:00 +00:00
|
|
|
_, err = svc.GetMDMAppleEnrollmentProfileByToken(ctx, "foo", "")
|
2022-10-05 22:53:54 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
_, err = svc.GetMDMAppleInstallerDetailsByToken(ctx, "foo")
|
|
|
|
require.NoError(t, err)
|
2023-05-02 13:09:33 +00:00
|
|
|
_, err = svc.MDMAppleGetEULABytes(ctx, "foo")
|
|
|
|
require.NoError(t, err)
|
2022-12-19 13:37:08 +00:00
|
|
|
// 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)
|
2023-01-16 15:22:12 +00:00
|
|
|
|
|
|
|
// Must be device-authenticated, should fail
|
|
|
|
_, err = svc.GetDeviceMDMAppleEnrollmentProfile(ctx)
|
|
|
|
checkAuthErr(t, err, true)
|
|
|
|
// works with device-authenticated context
|
|
|
|
ctx = test.HostContext(context.Background(), &fleet.Host{})
|
|
|
|
_, err = svc.GetDeviceMDMAppleEnrollmentProfile(ctx)
|
|
|
|
require.NoError(t, err)
|
2023-04-03 18:25:49 +00:00
|
|
|
|
|
|
|
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>`))
|
|
|
|
|
2023-04-17 15:45:16 +00:00
|
|
|
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)
|
2023-05-12 16:50:20 +00:00
|
|
|
_, _, err = svc.EnqueueMDMAppleCommand(ctx, rawB64FreeCmd, c.uuids)
|
2023-04-17 15:45:16 +00:00
|
|
|
checkAuthErr(t, err, c.shoudFailWithAuth)
|
|
|
|
})
|
|
|
|
}
|
2023-04-03 18:25:49 +00:00
|
|
|
|
2023-04-17 15:45:16 +00:00
|
|
|
// 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"?>
|
2023-04-03 18:25:49 +00:00
|
|
|
<!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")))
|
2023-05-12 16:50:20 +00:00
|
|
|
_, _, err = svc.EnqueueMDMAppleCommand(ctx, rawB64PremiumCmd, []string{"host1"})
|
2023-04-17 15:45:16 +00:00
|
|
|
require.Error(t, err)
|
|
|
|
require.ErrorContains(t, err, fleet.ErrMissingLicense.Error())
|
|
|
|
})
|
2023-04-05 14:50:36 +00:00
|
|
|
|
|
|
|
cmdUUIDToHostUUIDs := map[string][]string{
|
|
|
|
"uuidTm1": {"host1", "host2"},
|
|
|
|
"uuidTm2": {"host3"},
|
|
|
|
"uuidNoTm": {"host4"},
|
|
|
|
"uuidMixTm1Tm2": {"host1", "host3"},
|
|
|
|
}
|
|
|
|
ds.GetMDMAppleCommandResultsFunc = func(ctx context.Context, commandUUID string) ([]*fleet.MDMAppleCommandResult, error) {
|
|
|
|
hosts := cmdUUIDToHostUUIDs[commandUUID]
|
|
|
|
res := make([]*fleet.MDMAppleCommandResult, 0, len(hosts))
|
|
|
|
for _, h := range hosts {
|
|
|
|
res = append(res, &fleet.MDMAppleCommandResult{
|
|
|
|
DeviceID: h,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2023-04-17 15:45:16 +00:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("ListMDMAppleCommands", func(t *testing.T) {
|
|
|
|
ds.ListMDMAppleCommandsFunc = func(ctx context.Context, tmFilter fleet.TeamFilter, opt *fleet.MDMAppleCommandListOptions) ([]*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 {
|
2023-04-17 17:37:52 +00:00
|
|
|
desc string
|
|
|
|
user *fleet.User
|
|
|
|
want []string // the expected device ids in the results
|
|
|
|
shouldFail bool // with forbidden error
|
2023-04-17 15:45:16 +00:00
|
|
|
}{
|
2023-04-17 17:37:52 +00:00
|
|
|
{"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},
|
2023-04-17 15:45:16 +00:00
|
|
|
}
|
|
|
|
for _, c := range listCmdsCases {
|
|
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
|
|
ctx = test.UserContext(ctx, c.user)
|
|
|
|
res, err := svc.ListMDMAppleCommands(ctx, &fleet.MDMAppleCommandListOptions{})
|
2023-04-17 17:37:52 +00:00
|
|
|
checkAuthErr(t, err, c.shouldFail)
|
|
|
|
if c.shouldFail {
|
|
|
|
return
|
|
|
|
}
|
2023-04-17 15:45:16 +00:00
|
|
|
|
|
|
|
got := make([]string, len(res))
|
|
|
|
for i, r := range res {
|
|
|
|
got[i] = r.DeviceID
|
|
|
|
}
|
|
|
|
require.Equal(t, c.want, got)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2022-10-05 22:53:54 +00:00
|
|
|
}
|
2022-12-23 17:55:17 +00:00
|
|
|
|
2023-02-17 15:28:28 +00:00
|
|
|
func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-02-17 15:28:28 +00:00
|
|
|
|
|
|
|
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) (*fleet.MDMAppleConfigProfile, error) {
|
|
|
|
return &cp, nil
|
|
|
|
}
|
|
|
|
ds.ListMDMAppleConfigProfilesFunc = func(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-04-22 15:23:38 +00:00
|
|
|
ds.GetMDMAppleHostsProfilesSummaryFunc = func(context.Context, *uint) (*fleet.MDMAppleConfigProfilesSummary, error) {
|
2023-03-02 00:36:59 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
2023-03-27 18:43:01 +00:00
|
|
|
ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hids, tids, pids []uint, uuids []string) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-17 15:28:28 +00:00
|
|
|
mockGetFuncWithTeamID := func(teamID uint) mock.GetMDMAppleConfigProfileFunc {
|
|
|
|
return func(ctx context.Context, profileID uint) (*fleet.MDMAppleConfigProfile, error) {
|
|
|
|
require.Equal(t, uint(42), profileID)
|
|
|
|
return &fleet.MDMAppleConfigProfile{TeamID: &teamID}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mockDeleteFuncWithTeamID := func(teamID uint) mock.DeleteMDMAppleConfigProfileFunc {
|
|
|
|
return func(ctx context.Context, profileID uint) error {
|
|
|
|
require.Equal(t, uint(42), profileID)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mockTeamFuncWithUser := func(u *fleet.User) mock.TeamFunc {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.TeamFunc = mockTeamFuncWithUser(tt.user)
|
|
|
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
// test authz create new profile (no team)
|
|
|
|
_, err := svc.NewMDMAppleConfigProfile(ctx, 0, bytes.NewReader(mcBytes), int64(len(mcBytes)))
|
|
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
|
|
|
|
// test authz create new profile (team 1)
|
|
|
|
_, err = svc.NewMDMAppleConfigProfile(ctx, 1, bytes.NewReader(mcBytes), int64(len(mcBytes)))
|
|
|
|
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, 42)
|
|
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
|
|
|
|
// test authz delete config profile (no team)
|
|
|
|
ds.DeleteMDMAppleConfigProfileFunc = mockDeleteFuncWithTeamID(0)
|
|
|
|
err = svc.DeleteMDMAppleConfigProfile(ctx, 42)
|
|
|
|
checkShouldFail(err, tt.shouldFailGlobal)
|
|
|
|
|
|
|
|
// test authz get config profile (team 1)
|
|
|
|
ds.GetMDMAppleConfigProfileFunc = mockGetFuncWithTeamID(1)
|
|
|
|
_, err = svc.GetMDMAppleConfigProfile(ctx, 42)
|
|
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
|
|
|
|
|
|
|
// test authz delete config profile (team 1)
|
|
|
|
ds.DeleteMDMAppleConfigProfileFunc = mockDeleteFuncWithTeamID(1)
|
|
|
|
err = svc.DeleteMDMAppleConfigProfile(ctx, 42)
|
|
|
|
checkShouldFail(err, tt.shouldFailTeam)
|
2023-03-02 00:36:59 +00:00
|
|
|
|
|
|
|
// 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)
|
2023-02-17 15:28:28 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNewMDMAppleConfigProfile(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-02-17 15:28:28 +00:00
|
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
|
|
|
|
mcBytes := mcBytesForTest("Foo", "Bar", "UUID")
|
|
|
|
r := bytes.NewReader(mcBytes)
|
|
|
|
|
|
|
|
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, cp fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
|
|
|
|
require.Equal(t, "Foo", cp.Name)
|
|
|
|
require.Equal(t, "Bar", cp.Identifier)
|
|
|
|
require.Equal(t, mcBytes, []byte(cp.Mobileconfig))
|
|
|
|
cp.ProfileID = 1
|
|
|
|
return &cp, nil
|
|
|
|
}
|
|
|
|
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-03-27 18:43:01 +00:00
|
|
|
ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hids, tids, pids []uint, uuids []string) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-17 15:28:28 +00:00
|
|
|
|
|
|
|
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, r.Size())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "Foo", cp.Name)
|
|
|
|
require.Equal(t, "Bar", cp.Identifier)
|
|
|
|
require.Equal(t, mcBytes, []byte(cp.Mobileconfig))
|
|
|
|
}
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2023-02-22 22:26:06 +00:00
|
|
|
func TestHostDetailsMDMProfiles(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-02-22 22:26:06 +00:00
|
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
|
|
|
|
|
|
|
expected := []fleet.HostMDMAppleProfile{
|
2023-02-23 13:27:00 +00:00
|
|
|
{HostUUID: "H057-UU1D-1337", Name: "NAME-5", ProfileID: uint(5), CommandUUID: "CMD-UU1D-5", Status: &fleet.MDMAppleDeliveryPending, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""},
|
2023-04-24 21:27:15 +00:00
|
|
|
{HostUUID: "H057-UU1D-1337", Name: "NAME-9", ProfileID: uint(8), CommandUUID: "CMD-UU1D-8", Status: &fleet.MDMAppleDeliveryVerifying, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""},
|
2023-02-23 13:27:00 +00:00
|
|
|
{HostUUID: "H057-UU1D-1337", Name: "NAME-13", ProfileID: uint(13), CommandUUID: "CMD-UU1D-13", Status: &fleet.MDMAppleDeliveryFailed, OperationType: fleet.MDMAppleOperationTypeRemove, Detail: "Error removing profile"},
|
2023-02-22 22:26:06 +00:00
|
|
|
}
|
|
|
|
expectedByProfileID := make(map[uint]fleet.HostMDMAppleProfile)
|
|
|
|
for _, ep := range expected {
|
|
|
|
expectedByProfileID[ep.ProfileID] = ep
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.GetHostMDMProfilesFunc = 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"}, nil
|
|
|
|
}
|
|
|
|
return &fleet.Host{ID: hostID, UUID: "WR0N6-UU1D"}, 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"}, nil
|
|
|
|
}
|
|
|
|
return &fleet.Host{ID: uint(21), UUID: "WR0N6-UU1D"}, 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.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2023-04-22 15:23:38 +00:00
|
|
|
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2023-02-22 22:26:06 +00:00
|
|
|
|
2023-02-23 13:27:00 +00:00
|
|
|
expectedNilSlice := []fleet.HostMDMAppleProfile(nil)
|
|
|
|
expectedEmptySlice := []fleet.HostMDMAppleProfile{}
|
|
|
|
|
2023-02-22 22:26:06 +00:00
|
|
|
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",
|
2023-02-23 13:27:00 +00:00
|
|
|
mdmEnabled: true,
|
2023-02-22 22:26:06 +00:00
|
|
|
hostID: ptr.Uint(21),
|
|
|
|
hostIdentifier: nil,
|
2023-02-23 13:27:00 +00:00
|
|
|
expected: &expectedEmptySlice,
|
2023-02-22 22:26:06 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "TestGetHostMDMProfilesNil",
|
|
|
|
mdmEnabled: false,
|
|
|
|
hostID: ptr.Uint(42),
|
|
|
|
hostIdentifier: nil,
|
2023-02-23 13:27:00 +00:00
|
|
|
expected: &expectedNilSlice,
|
2023-02-22 22:26:06 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "TestHostByIdentifierMDMProfilesOK",
|
|
|
|
mdmEnabled: true,
|
|
|
|
hostID: nil,
|
|
|
|
hostIdentifier: ptr.String("h0571d3n71f13r"),
|
|
|
|
expected: &expected,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "TestHostByIdentifierMDMProfilesNil",
|
|
|
|
mdmEnabled: false,
|
|
|
|
hostID: nil,
|
|
|
|
hostIdentifier: ptr.String("h0571d3n71f13r"),
|
2023-02-23 13:27:00 +00:00
|
|
|
expected: &expectedNilSlice,
|
2023-02-22 22:26:06 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "TestHostByIdentifierMDMProfilesEmpty",
|
2023-02-23 13:27:00 +00:00
|
|
|
mdmEnabled: true,
|
2023-02-22 22:26:06 +00:00
|
|
|
hostID: nil,
|
|
|
|
hostIdentifier: ptr.String("4n07h3r1d3n71f13r"),
|
2023-02-23 13:27:00 +00:00
|
|
|
expected: &expectedEmptySlice,
|
2023-02-22 22:26:06 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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.GetHostMDMProfilesFuncInvoked = 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 {
|
2023-02-23 13:27:00 +00:00
|
|
|
require.Equal(t, gotHost.MDM.Profiles, c.expected)
|
2023-02-22 22:26:06 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
require.True(t, ds.GetHostMDMProfilesFuncInvoked)
|
|
|
|
require.NotNil(t, gotHost.MDM.Profiles)
|
|
|
|
require.ElementsMatch(t, *c.expected, *gotHost.MDM.Profiles)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-23 23:05:24 +00:00
|
|
|
func TestMDMCommandAuthz(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-01-23 23:05:24 +00:00
|
|
|
|
|
|
|
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)}, nil
|
|
|
|
default:
|
|
|
|
return &fleet.Host{UUID: "test-host-no-team"}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
|
|
return &fleet.HostMDMCheckinInfo{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var mdmEnabled atomic.Bool
|
2023-03-27 18:43:01 +00:00
|
|
|
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
2023-01-23 23:05:24 +00:00
|
|
|
// 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.
|
2023-03-27 18:43:01 +00:00
|
|
|
enroll := fleet.NanoEnrollment{
|
|
|
|
Enabled: mdmEnabled.Swap(!mdmEnabled.Load()),
|
|
|
|
}
|
|
|
|
return &enroll, nil
|
2023-01-23 23:05:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.EnqueueMDMAppleCommandRemoveEnrollmentProfile(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.EnqueueMDMAppleCommandRemoveEnrollmentProfile(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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-16 20:06:30 +00:00
|
|
|
func TestMDMAuthenticate(t *testing.T) {
|
|
|
|
ds := new(mock.Store)
|
|
|
|
svc := MDMAppleCheckinAndCommandService{ds: ds}
|
|
|
|
ctx := context.Background()
|
|
|
|
uuid, serial, model := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1"
|
|
|
|
|
|
|
|
ds.IngestMDMAppleDeviceFromCheckinFunc = func(ctx context.Context, mdmHost fleet.MDMAppleHostDetails) error {
|
|
|
|
require.Equal(t, uuid, mdmHost.UDID)
|
|
|
|
require.Equal(t, serial, mdmHost.SerialNumber)
|
|
|
|
require.Equal(t, model, mdmHost.Model)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
|
|
require.Equal(t, uuid, hostUUID)
|
2023-01-23 23:05:24 +00:00
|
|
|
return &fleet.HostMDMCheckinInfo{HardwareSerial: serial, DisplayName: fmt.Sprintf("%s (%s)", model, serial), InstalledFromDEP: false}, nil
|
2023-01-16 20:06:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
|
|
a, ok := activity.(*fleet.ActivityTypeMDMEnrolled)
|
|
|
|
require.True(t, ok)
|
|
|
|
require.Nil(t, user)
|
|
|
|
require.Equal(t, "mdm_enrolled", activity.ActivityName())
|
|
|
|
require.Equal(t, serial, a.HostSerial)
|
2023-01-23 23:05:24 +00:00
|
|
|
require.Equal(t, a.HostDisplayName, fmt.Sprintf("%s (%s)", model, serial))
|
2023-01-16 20:06:30 +00:00
|
|
|
require.False(t, a.InstalledFromDEP)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := svc.Authenticate(
|
|
|
|
&mdm.Request{Context: ctx},
|
|
|
|
&mdm.Authenticate{
|
|
|
|
Enrollment: mdm.Enrollment{
|
|
|
|
UDID: uuid,
|
|
|
|
},
|
|
|
|
SerialNumber: serial,
|
|
|
|
Model: model,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.IngestMDMAppleDeviceFromCheckinFuncInvoked)
|
|
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
|
|
require.True(t, ds.NewActivityFuncInvoked)
|
|
|
|
}
|
|
|
|
|
2023-04-05 23:52:26 +00:00
|
|
|
func TestMDMTokenUpdate(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
ds := new(mock.Store)
|
|
|
|
mdmStorage := &nanomdm_mock.Storage{}
|
|
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
|
|
pusher := nanomdm_pushsvc.New(
|
|
|
|
mdmStorage,
|
|
|
|
mdmStorage,
|
|
|
|
pushFactory,
|
|
|
|
NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)),
|
|
|
|
)
|
|
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
2023-04-07 20:31:02 +00:00
|
|
|
svc := MDMAppleCheckinAndCommandService{ds: ds, commander: cmdr, logger: kitlog.NewNopLogger()}
|
|
|
|
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
|
|
|
|
serverURL := "https://example.com"
|
2023-05-18 15:50:00 +00:00
|
|
|
commands := map[string]int{}
|
2023-04-05 23:52:26 +00:00
|
|
|
|
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
2023-05-18 15:50:00 +00:00
|
|
|
if _, ok := commands[cmd.Command.RequestType]; !ok {
|
|
|
|
commands[cmd.Command.RequestType] = 0
|
|
|
|
}
|
|
|
|
commands[cmd.Command.RequestType]++
|
2023-04-05 23:52:26 +00:00
|
|
|
require.NotNil(t, cmd)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
mdmStorage.RetrievePushInfoFunc = func(p0 context.Context, targetUUIDs []string) (map[string]*mdm.Push, error) {
|
|
|
|
require.ElementsMatch(t, []string{uuid}, targetUUIDs)
|
|
|
|
pushes := make(map[string]*mdm.Push, len(targetUUIDs))
|
|
|
|
for _, uuid := range targetUUIDs {
|
|
|
|
pushes[uuid] = &mdm.Push{
|
|
|
|
PushMagic: "magic" + uuid,
|
|
|
|
Token: []byte("token" + uuid),
|
|
|
|
Topic: "topic" + uuid,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return pushes, 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
|
|
|
|
}
|
|
|
|
|
2023-04-07 20:31:02 +00:00
|
|
|
mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
|
|
appCfg := &fleet.AppConfig{}
|
|
|
|
appCfg.ServerSettings.ServerURL = serverURL
|
2023-05-18 15:50:00 +00:00
|
|
|
// assign a name to simulate EndUserAuthentication being configured
|
|
|
|
appCfg.MDM.EndUserAuthentication.IDPName = "FooIdP"
|
2023-04-07 20:31:02 +00:00
|
|
|
return appCfg, nil
|
|
|
|
}
|
2023-04-05 23:52:26 +00:00
|
|
|
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{
|
|
|
|
HardwareSerial: serial,
|
|
|
|
DisplayName: model,
|
|
|
|
InstalledFromDEP: true,
|
2023-04-07 20:31:02 +00:00
|
|
|
TeamID: wantTeamID,
|
2023-04-05 23:52:26 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hids, tids, pids []uint, uuids []string) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-07 20:31:02 +00:00
|
|
|
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
|
|
|
|
require.Equal(t, wantTeamID, teamID)
|
|
|
|
return &fleet.MDMAppleBootstrapPackage{}, nil
|
|
|
|
}
|
2023-04-22 15:23:38 +00:00
|
|
|
ds.RecordHostBootstrapPackageFunc = func(ctx context.Context, commandUUID string, hostUUID string) error {
|
|
|
|
require.Equal(t, uuid, hostUUID)
|
|
|
|
require.NotEmpty(t, commandUUID)
|
|
|
|
return nil
|
|
|
|
}
|
2023-05-18 15:50:00 +00:00
|
|
|
idpAcc := &fleet.MDMIdPAccount{
|
|
|
|
UUID: "FOO-BAR",
|
|
|
|
Fullname: "Jane Doe",
|
|
|
|
Username: "jane.doe@example.com",
|
|
|
|
}
|
|
|
|
ds.GetMDMIdPAccountFunc = func(ctx context.Context, uuid string) (*fleet.MDMIdPAccount, error) {
|
|
|
|
require.Equal(t, idpAcc.UUID, uuid)
|
|
|
|
return idpAcc, nil
|
|
|
|
}
|
|
|
|
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
|
|
|
tm := &fleet.Team{}
|
|
|
|
tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true
|
|
|
|
return tm, nil
|
|
|
|
}
|
2023-04-22 15:23:38 +00:00
|
|
|
|
2023-04-05 23:52:26 +00:00
|
|
|
err := svc.TokenUpdate(
|
|
|
|
&mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}},
|
|
|
|
&mdm.TokenUpdate{
|
|
|
|
Enrollment: mdm.Enrollment{
|
|
|
|
UDID: uuid,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.BulkSetPendingMDMAppleHostProfilesFuncInvoked)
|
|
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
2023-04-07 20:31:02 +00:00
|
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
2023-04-22 15:23:38 +00:00
|
|
|
require.True(t, ds.RecordHostBootstrapPackageFuncInvoked)
|
2023-05-18 15:50:00 +00:00
|
|
|
require.False(t, ds.GetMDMIdPAccountFuncInvoked)
|
|
|
|
require.Equal(t, 2, commands["InstallEnterpriseApplication"])
|
|
|
|
require.Equal(t, 0, commands["AccountConfiguration"])
|
|
|
|
ds.BulkSetPendingMDMAppleHostProfilesFuncInvoked = false
|
|
|
|
ds.GetHostMDMCheckinInfoFuncInvoked = false
|
|
|
|
ds.AppConfigFuncInvoked = false
|
|
|
|
ds.RecordHostBootstrapPackageFuncInvoked = false
|
|
|
|
commands["InstallEnterpriseApplication"] = 0
|
|
|
|
|
|
|
|
// with enrollment reference
|
|
|
|
err = svc.TokenUpdate(
|
|
|
|
&mdm.Request{
|
|
|
|
Context: ctx,
|
|
|
|
EnrollID: &mdm.EnrollID{ID: uuid},
|
|
|
|
Params: map[string]string{"enroll_reference": idpAcc.UUID},
|
|
|
|
},
|
|
|
|
&mdm.TokenUpdate{
|
|
|
|
Enrollment: mdm.Enrollment{
|
|
|
|
UDID: uuid,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.BulkSetPendingMDMAppleHostProfilesFuncInvoked)
|
|
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
|
|
require.True(t, ds.RecordHostBootstrapPackageFuncInvoked)
|
|
|
|
require.True(t, ds.GetMDMIdPAccountFuncInvoked)
|
|
|
|
require.Equal(t, 2, commands["InstallEnterpriseApplication"])
|
|
|
|
require.Equal(t, 1, commands["AccountConfiguration"])
|
2023-04-05 23:52:26 +00:00
|
|
|
}
|
|
|
|
|
2023-01-16 20:06:30 +00:00
|
|
|
func TestMDMCheckout(t *testing.T) {
|
|
|
|
ds := new(mock.Store)
|
|
|
|
svc := MDMAppleCheckinAndCommandService{ds: ds}
|
|
|
|
ctx := context.Background()
|
2023-01-23 23:05:24 +00:00
|
|
|
uuid, serial, installedFromDEP, displayName := "ABC-DEF-GHI", "XYZABC", true, "Test's MacBook"
|
2023-01-16 20:06:30 +00:00
|
|
|
|
|
|
|
ds.UpdateHostTablesOnMDMUnenrollFunc = func(ctx context.Context, hostUUID string) error {
|
|
|
|
require.Equal(t, uuid, hostUUID)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
|
|
|
require.Equal(t, uuid, hostUUID)
|
|
|
|
return &fleet.HostMDMCheckinInfo{
|
|
|
|
HardwareSerial: serial,
|
2023-01-23 23:05:24 +00:00
|
|
|
DisplayName: displayName,
|
2023-01-16 20:06:30 +00:00
|
|
|
InstalledFromDEP: installedFromDEP,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
|
|
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)
|
2023-01-23 23:05:24 +00:00
|
|
|
require.Equal(t, displayName, a.HostDisplayName)
|
2023-01-16 20:06:30 +00:00
|
|
|
require.True(t, a.InstalledFromDEP)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := svc.CheckOut(
|
|
|
|
&mdm.Request{Context: ctx},
|
|
|
|
&mdm.CheckOut{
|
|
|
|
Enrollment: mdm.Enrollment{
|
|
|
|
UDID: uuid,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.UpdateHostTablesOnMDMUnenrollFuncInvoked)
|
|
|
|
require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked)
|
|
|
|
require.True(t, ds.NewActivityFuncInvoked)
|
|
|
|
}
|
2023-02-15 18:01:44 +00:00
|
|
|
|
2023-02-22 17:49:06 +00:00
|
|
|
func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) {
|
|
|
|
ds := new(mock.Store)
|
|
|
|
svc := MDMAppleCheckinAndCommandService{ds: ds}
|
|
|
|
ctx := context.Background()
|
|
|
|
hostUUID := "ABC-DEF-GHI"
|
|
|
|
commandUUID := "COMMAND-UUID"
|
|
|
|
|
|
|
|
cases := []struct {
|
|
|
|
status string
|
|
|
|
requestType string
|
|
|
|
errors []mdm.ErrorChain
|
|
|
|
want *fleet.HostMDMAppleProfile
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
status: "Acknowledged",
|
|
|
|
requestType: "InstallProfile",
|
|
|
|
errors: nil,
|
|
|
|
want: &fleet.HostMDMAppleProfile{
|
2023-04-24 21:27:15 +00:00
|
|
|
Status: &fleet.MDMAppleDeliveryVerifying,
|
2023-02-22 17:49:06 +00:00
|
|
|
Detail: "",
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
status: "Acknowledged",
|
|
|
|
requestType: "RemoveProfile",
|
|
|
|
errors: nil,
|
|
|
|
want: &fleet.HostMDMAppleProfile{
|
2023-04-24 21:27:15 +00:00
|
|
|
Status: &fleet.MDMAppleDeliveryVerifying,
|
2023-02-22 17:49:06 +00:00
|
|
|
Detail: "",
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
status: "Error",
|
|
|
|
requestType: "InstallProfile",
|
|
|
|
errors: []mdm.ErrorChain{
|
|
|
|
{ErrorCode: 123, ErrorDomain: "testDomain", USEnglishDescription: "testMessage"},
|
|
|
|
},
|
|
|
|
want: &fleet.HostMDMAppleProfile{
|
|
|
|
Status: &fleet.MDMAppleDeliveryFailed,
|
|
|
|
Detail: "testDomain (123): testMessage\n",
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
status: "Error",
|
|
|
|
requestType: "RemoveProfile",
|
|
|
|
errors: []mdm.ErrorChain{
|
|
|
|
{ErrorCode: 123, ErrorDomain: "testDomain", USEnglishDescription: "testMessage"},
|
|
|
|
{ErrorCode: 321, ErrorDomain: "domainTest", USEnglishDescription: "messageTest"},
|
|
|
|
},
|
|
|
|
want: &fleet.HostMDMAppleProfile{
|
|
|
|
Status: &fleet.MDMAppleDeliveryFailed,
|
|
|
|
Detail: "testDomain (123): testMessage\ndomainTest (321): messageTest\n",
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
status: "Error",
|
|
|
|
requestType: "RemoveProfile",
|
|
|
|
errors: nil,
|
|
|
|
want: &fleet.HostMDMAppleProfile{
|
|
|
|
Status: &fleet.MDMAppleDeliveryFailed,
|
|
|
|
Detail: "",
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range cases {
|
|
|
|
ds.GetMDMAppleCommandRequestTypeFunc = func(ctx context.Context, targetCmd string) (string, error) {
|
|
|
|
require.Equal(t, commandUUID, targetCmd)
|
|
|
|
return c.requestType, nil
|
|
|
|
}
|
|
|
|
|
2023-03-08 20:42:23 +00:00
|
|
|
ds.UpdateOrDeleteHostMDMAppleProfileFunc = func(ctx context.Context, profile *fleet.HostMDMAppleProfile) error {
|
2023-02-22 17:49:06 +00:00
|
|
|
c.want.CommandUUID = commandUUID
|
|
|
|
c.want.HostUUID = hostUUID
|
|
|
|
require.Equal(t, c.want, profile)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := svc.CommandAndReportResults(
|
|
|
|
&mdm.Request{Context: ctx},
|
|
|
|
&mdm.CommandResults{
|
|
|
|
Enrollment: mdm.Enrollment{UDID: hostUUID},
|
|
|
|
CommandUUID: commandUUID,
|
|
|
|
Status: c.status,
|
|
|
|
RequestType: c.requestType,
|
|
|
|
ErrorChain: c.errors,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.GetMDMAppleCommandRequestTypeFuncInvoked)
|
2023-03-08 20:42:23 +00:00
|
|
|
require.True(t, ds.UpdateOrDeleteHostMDMAppleProfileFuncInvoked)
|
2023-02-22 17:49:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-15 18:01:44 +00:00
|
|
|
func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-02-15 18:01:44 +00:00
|
|
|
|
|
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
|
|
return &fleet.Team{ID: 1, Name: name}, nil
|
|
|
|
}
|
2023-02-16 16:53:26 +00:00
|
|
|
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
|
|
return &fleet.Team{ID: id, Name: "team"}, nil
|
|
|
|
}
|
2023-02-15 18:01:44 +00:00
|
|
|
ds.BatchSetMDMAppleProfilesFunc = func(ctx context.Context, teamID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-16 16:53:26 +00:00
|
|
|
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-03-27 18:43:01 +00:00
|
|
|
ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hids, tids, pids []uint, uuids []string) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-15 18:01:44 +00:00
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
user *fleet.User
|
|
|
|
premium bool
|
|
|
|
teamID *uint
|
|
|
|
teamName *string
|
|
|
|
profiles [][]byte
|
|
|
|
wantErr string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
"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"),
|
|
|
|
},
|
|
|
|
``,
|
|
|
|
},
|
2023-02-24 20:12:53 +00:00
|
|
|
{
|
|
|
|
"unsupported payload type",
|
|
|
|
&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>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>com.apple.MCX.FileVault2</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>`)},
|
|
|
|
"unsupported PayloadType(s)",
|
|
|
|
},
|
2023-02-15 18:01:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-06 14:54:51 +00:00
|
|
|
func TestUpdateMDMAppleSettings(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-03-06 14:54:51 +00:00
|
|
|
|
|
|
|
ds.TeamFunc = 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.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
|
|
return 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,
|
|
|
|
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,
|
|
|
|
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),
|
|
|
|
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.UpdateMDMAppleSettings(ctx, fleet.MDMAppleSettingsPayload{TeamID: tt.teamID})
|
|
|
|
if tt.wantErr == "" {
|
|
|
|
require.NoError(t, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.Error(t, err)
|
|
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-10 20:22:08 +00:00
|
|
|
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.TeamFunc = 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.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
|
|
return 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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-02-17 19:26:51 +00:00
|
|
|
func TestMDMAppleCommander(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
mdmStorage := &nanomdm_mock.Storage{}
|
|
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
|
|
pusher := nanomdm_pushsvc.New(
|
|
|
|
mdmStorage,
|
|
|
|
mdmStorage,
|
|
|
|
pushFactory,
|
|
|
|
NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)),
|
|
|
|
)
|
2023-04-05 14:50:36 +00:00
|
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
2023-02-17 19:26:51 +00:00
|
|
|
|
|
|
|
// TODO(roberto): there's a data race in the mock when more
|
|
|
|
// than one host ID is provided because the pusher uses one
|
|
|
|
// goroutine per uuid to send the commands
|
|
|
|
hostUUIDs := []string{"A"}
|
|
|
|
payloadName := "com.foo.bar"
|
|
|
|
payloadIdentifier := "com-foo-bar"
|
|
|
|
mc := mobileconfigForTest(payloadName, payloadIdentifier)
|
|
|
|
|
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
|
|
|
require.NotNil(t, cmd)
|
|
|
|
require.Equal(t, cmd.Command.RequestType, "InstallProfile")
|
|
|
|
require.Contains(t, string(cmd.Raw), base64.StdEncoding.EncodeToString(mc))
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
mdmStorage.RetrievePushInfoFunc = func(p0 context.Context, targetUUIDs []string) (map[string]*mdm.Push, error) {
|
|
|
|
require.ElementsMatch(t, hostUUIDs, targetUUIDs)
|
|
|
|
pushes := make(map[string]*mdm.Push, len(targetUUIDs))
|
|
|
|
for _, uuid := range targetUUIDs {
|
|
|
|
pushes[uuid] = &mdm.Push{
|
|
|
|
PushMagic: "magic" + uuid,
|
|
|
|
Token: []byte("token" + uuid),
|
|
|
|
Topic: "topic" + uuid,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return pushes, 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
|
|
|
|
}
|
|
|
|
|
2023-02-22 17:49:06 +00:00
|
|
|
cmdUUID := uuid.New().String()
|
|
|
|
err := cmdr.InstallProfile(ctx, hostUUIDs, mc, cmdUUID)
|
2023-02-17 19:26:51 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
|
|
|
mdmStorage.EnqueueCommandFuncInvoked = false
|
|
|
|
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
|
|
|
mdmStorage.RetrievePushInfoFuncInvoked = false
|
|
|
|
|
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
|
|
|
require.NotNil(t, cmd)
|
|
|
|
require.Equal(t, "RemoveProfile", cmd.Command.RequestType)
|
|
|
|
require.Contains(t, string(cmd.Raw), payloadIdentifier)
|
|
|
|
return nil, nil
|
|
|
|
}
|
2023-02-22 17:49:06 +00:00
|
|
|
cmdUUID = uuid.New().String()
|
|
|
|
err = cmdr.RemoveProfile(ctx, hostUUIDs, payloadIdentifier, cmdUUID)
|
2023-02-17 19:26:51 +00:00
|
|
|
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
|
|
|
mdmStorage.EnqueueCommandFuncInvoked = false
|
|
|
|
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
|
|
|
mdmStorage.RetrievePushInfoFuncInvoked = false
|
2023-02-22 17:49:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-04-05 23:52:26 +00:00
|
|
|
|
|
|
|
cmdUUID = uuid.New().String()
|
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
|
|
|
require.NotNil(t, cmd)
|
|
|
|
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
|
|
|
|
require.Contains(t, string(cmd.Raw), "http://test.example.com")
|
|
|
|
require.Contains(t, string(cmd.Raw), cmdUUID)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
err = cmdr.InstallEnterpriseApplication(ctx, hostUUIDs, "http://test.example.com", cmdUUID)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
|
|
|
mdmStorage.EnqueueCommandFuncInvoked = false
|
|
|
|
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
|
|
|
mdmStorage.RetrievePushInfoFuncInvoked = false
|
2023-02-22 17:49:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestMDMAppleReconcileProfiles(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
mdmStorage := &nanomdm_mock.Storage{}
|
|
|
|
ds := new(mock.Store)
|
|
|
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
|
|
|
pusher := nanomdm_pushsvc.New(
|
|
|
|
mdmStorage,
|
|
|
|
mdmStorage,
|
|
|
|
pushFactory,
|
|
|
|
NewNanoMDMLogger(kitlog.NewNopLogger()),
|
|
|
|
)
|
2023-04-05 14:50:36 +00:00
|
|
|
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
2023-03-27 18:43:01 +00:00
|
|
|
hostUUID, hostUUID2 := "ABC-DEF", "GHI-JKL"
|
2023-02-22 17:49:06 +00:00
|
|
|
contents1 := []byte("test-content-1")
|
|
|
|
contents1Base64 := base64.StdEncoding.EncodeToString(contents1)
|
|
|
|
contents2 := []byte("test-content-2")
|
|
|
|
contents2Base64 := base64.StdEncoding.EncodeToString(contents2)
|
2023-03-27 18:43:01 +00:00
|
|
|
contents4 := []byte("test-content-4")
|
|
|
|
contents4Base64 := base64.StdEncoding.EncodeToString(contents4)
|
2023-02-22 17:49:06 +00:00
|
|
|
|
|
|
|
ds.ListMDMAppleProfilesToInstallFunc = func(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
|
|
|
return []*fleet.MDMAppleProfilePayload{
|
|
|
|
{ProfileID: 1, ProfileIdentifier: "com.add.profile", HostUUID: hostUUID},
|
|
|
|
{ProfileID: 2, ProfileIdentifier: "com.add.profile.two", HostUUID: hostUUID},
|
2023-03-27 18:43:01 +00:00
|
|
|
{ProfileID: 2, ProfileIdentifier: "com.add.profile.two", HostUUID: hostUUID2},
|
|
|
|
{ProfileID: 4, ProfileIdentifier: "com.add.profile.four", HostUUID: hostUUID2},
|
2023-02-22 17:49:06 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ds.ListMDMAppleProfilesToRemoveFunc = func(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
|
|
|
|
return []*fleet.MDMAppleProfilePayload{
|
|
|
|
{ProfileID: 3, ProfileIdentifier: "com.remove.profile", HostUUID: hostUUID},
|
2023-03-27 18:43:01 +00:00
|
|
|
{ProfileID: 3, ProfileIdentifier: "com.remove.profile", HostUUID: hostUUID2},
|
2023-02-22 17:49:06 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-03-17 21:52:30 +00:00
|
|
|
ds.GetMDMAppleProfilesContentsFunc = func(ctx context.Context, profileIDs []uint) (map[uint]mobileconfig.Mobileconfig, error) {
|
2023-03-27 18:43:01 +00:00
|
|
|
require.ElementsMatch(t, []uint{1, 2, 4}, profileIDs)
|
|
|
|
// only those profiles that are to be installed
|
2023-03-17 21:52:30 +00:00
|
|
|
return map[uint]mobileconfig.Mobileconfig{
|
2023-02-22 17:49:06 +00:00
|
|
|
1: contents1,
|
|
|
|
2: contents2,
|
2023-03-27 18:43:01 +00:00
|
|
|
4: contents4,
|
2023-02-22 17:49:06 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-03-27 18:43:01 +00:00
|
|
|
var enqueueFailForOp fleet.MDMAppleOperationType
|
2023-02-22 17:49:06 +00:00
|
|
|
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
|
|
|
require.NotNil(t, cmd)
|
2023-03-27 18:43:01 +00:00
|
|
|
require.NotEmpty(t, cmd.CommandUUID)
|
2023-02-22 17:49:06 +00:00
|
|
|
|
|
|
|
switch cmd.Command.RequestType {
|
|
|
|
case "InstallProfile":
|
2023-03-27 18:43:01 +00:00
|
|
|
// may be called for a single host or both
|
|
|
|
if len(id) == 2 {
|
|
|
|
require.ElementsMatch(t, []string{hostUUID, hostUUID2}, id)
|
|
|
|
} else {
|
|
|
|
require.Len(t, id, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !strings.Contains(string(cmd.Raw), contents1Base64) && !strings.Contains(string(cmd.Raw), contents2Base64) &&
|
|
|
|
!strings.Contains(string(cmd.Raw), contents4Base64) {
|
|
|
|
require.Failf(t, "profile contents don't match", "expected to contain %s, %s or %s but got %s",
|
|
|
|
contents1Base64, contents2Base64, contents4Base64, string(cmd.Raw))
|
2023-02-22 17:49:06 +00:00
|
|
|
}
|
|
|
|
case "RemoveProfile":
|
2023-03-27 18:43:01 +00:00
|
|
|
require.ElementsMatch(t, []string{hostUUID, hostUUID2}, id)
|
2023-02-22 17:49:06 +00:00
|
|
|
require.Contains(t, string(cmd.Raw), "com.remove.profile")
|
|
|
|
}
|
2023-03-27 18:43:01 +00:00
|
|
|
switch {
|
|
|
|
case enqueueFailForOp == fleet.MDMAppleOperationTypeInstall && cmd.Command.RequestType == "InstallProfile":
|
|
|
|
return nil, errors.New("enqueue error")
|
|
|
|
case enqueueFailForOp == fleet.MDMAppleOperationTypeRemove && cmd.Command.RequestType == "RemoveProfile":
|
|
|
|
return nil, errors.New("enqueue error")
|
|
|
|
}
|
2023-02-22 17:49:06 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-27 18:43:01 +00:00
|
|
|
var failedCall bool
|
|
|
|
var failedCheck func([]*fleet.MDMAppleBulkUpsertHostProfilePayload)
|
2023-02-22 17:49:06 +00:00
|
|
|
ds.BulkUpsertMDMAppleHostProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error {
|
2023-03-27 18:43:01 +00:00
|
|
|
if failedCall {
|
|
|
|
failedCheck(payload)
|
2023-02-22 17:49:06 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-03-27 18:43:01 +00:00
|
|
|
// 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
|
|
|
|
cmdUUIDByProfileIDInstall := make(map[uint]string)
|
|
|
|
cmdUUIDByProfileIDRemove := make(map[uint]string)
|
|
|
|
copies := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, len(payload))
|
|
|
|
for i, p := range payload {
|
|
|
|
if p.OperationType == fleet.MDMAppleOperationTypeInstall {
|
|
|
|
existing, ok := cmdUUIDByProfileIDInstall[p.ProfileID]
|
|
|
|
if ok {
|
|
|
|
require.Equal(t, existing, p.CommandUUID)
|
|
|
|
} else {
|
|
|
|
cmdUUIDByProfileIDInstall[p.ProfileID] = p.CommandUUID
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
require.Equal(t, fleet.MDMAppleOperationTypeRemove, p.OperationType)
|
|
|
|
existing, ok := cmdUUIDByProfileIDRemove[p.ProfileID]
|
|
|
|
if ok {
|
|
|
|
require.Equal(t, existing, p.CommandUUID)
|
|
|
|
} else {
|
|
|
|
cmdUUIDByProfileIDRemove[p.ProfileID] = p.CommandUUID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-02-22 17:49:06 +00:00
|
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
|
|
{
|
|
|
|
ProfileID: 1,
|
|
|
|
ProfileIdentifier: "com.add.profile",
|
|
|
|
HostUUID: hostUUID,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: &fleet.MDMAppleDeliveryPending,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 2,
|
|
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
|
|
HostUUID: hostUUID,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: &fleet.MDMAppleDeliveryPending,
|
2023-03-27 18:43:01 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 2,
|
|
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
|
|
HostUUID: hostUUID2,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: &fleet.MDMAppleDeliveryPending,
|
2023-02-22 17:49:06 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 3,
|
|
|
|
ProfileIdentifier: "com.remove.profile",
|
|
|
|
HostUUID: hostUUID,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
Status: &fleet.MDMAppleDeliveryPending,
|
|
|
|
},
|
2023-03-27 18:43:01 +00:00
|
|
|
{
|
|
|
|
ProfileID: 3,
|
|
|
|
ProfileIdentifier: "com.remove.profile",
|
|
|
|
HostUUID: hostUUID2,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
Status: &fleet.MDMAppleDeliveryPending,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 4,
|
|
|
|
ProfileIdentifier: "com.add.profile.four",
|
|
|
|
HostUUID: hostUUID2,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: &fleet.MDMAppleDeliveryPending,
|
|
|
|
},
|
|
|
|
}, copies)
|
2023-02-22 17:49:06 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-04 20:09:20 +00:00
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
|
|
appCfg := &fleet.AppConfig{}
|
|
|
|
appCfg.ServerSettings.ServerURL = "https://test.example.com"
|
|
|
|
return appCfg, 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
|
|
|
|
}
|
|
|
|
|
2023-03-27 18:43:01 +00:00
|
|
|
checkAndReset := func(t *testing.T, want bool, invoked *bool) {
|
|
|
|
if want {
|
|
|
|
require.True(t, *invoked)
|
|
|
|
} else {
|
|
|
|
require.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 := ReconcileProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, failedCount)
|
|
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
|
|
})
|
|
|
|
|
|
|
|
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, 2) // the 2 remove ops
|
|
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
|
|
{
|
|
|
|
ProfileID: 3,
|
|
|
|
ProfileIdentifier: "com.remove.profile",
|
|
|
|
HostUUID: hostUUID,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
Status: nil,
|
|
|
|
CommandUUID: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 3,
|
|
|
|
ProfileIdentifier: "com.remove.profile",
|
|
|
|
HostUUID: hostUUID2,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeRemove,
|
|
|
|
Status: nil,
|
|
|
|
CommandUUID: "",
|
|
|
|
},
|
|
|
|
}, payload)
|
|
|
|
}
|
|
|
|
|
|
|
|
enqueueFailForOp = fleet.MDMAppleOperationTypeRemove
|
|
|
|
err := ReconcileProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, failedCount)
|
|
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
|
|
})
|
|
|
|
|
|
|
|
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, 4) // the 4 install ops
|
|
|
|
require.ElementsMatch(t, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
|
|
|
{
|
|
|
|
ProfileID: 1,
|
|
|
|
ProfileIdentifier: "com.add.profile",
|
|
|
|
HostUUID: hostUUID,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: nil,
|
|
|
|
CommandUUID: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 2,
|
|
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
|
|
HostUUID: hostUUID,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: nil,
|
|
|
|
CommandUUID: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 2,
|
|
|
|
ProfileIdentifier: "com.add.profile.two",
|
|
|
|
HostUUID: hostUUID2,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: nil,
|
|
|
|
CommandUUID: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ProfileID: 4,
|
|
|
|
ProfileIdentifier: "com.add.profile.four",
|
|
|
|
HostUUID: hostUUID2,
|
|
|
|
OperationType: fleet.MDMAppleOperationTypeInstall,
|
|
|
|
Status: nil,
|
|
|
|
CommandUUID: "",
|
|
|
|
},
|
|
|
|
}, payload)
|
|
|
|
}
|
|
|
|
|
|
|
|
enqueueFailForOp = fleet.MDMAppleOperationTypeInstall
|
|
|
|
err := ReconcileProfiles(ctx, ds, cmdr, kitlog.NewNopLogger())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, failedCount)
|
|
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToInstallFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.ListMDMAppleProfilesToRemoveFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.GetMDMAppleProfilesContentsFuncInvoked)
|
|
|
|
checkAndReset(t, true, &ds.BulkUpsertMDMAppleHostProfilesFuncInvoked)
|
|
|
|
})
|
2023-02-17 19:26:51 +00:00
|
|
|
}
|
|
|
|
|
2023-03-01 13:43:15 +00:00
|
|
|
func TestAppleMDMFileVaultEscrowFunctions(t *testing.T) {
|
|
|
|
svc := Service{}
|
|
|
|
|
2023-03-08 13:31:53 +00:00
|
|
|
err := svc.MDMAppleEnableFileVaultAndEscrow(context.Background(), ptr.Uint(1))
|
2023-03-01 13:43:15 +00:00
|
|
|
require.ErrorIs(t, fleet.ErrMissingLicense, err)
|
|
|
|
|
2023-03-08 13:31:53 +00:00
|
|
|
err = svc.MDMAppleDisableFileVaultAndEscrow(context.Background(), ptr.Uint(1))
|
2023-03-01 13:43:15 +00:00
|
|
|
require.ErrorIs(t, fleet.ErrMissingLicense, err)
|
|
|
|
}
|
|
|
|
|
2023-03-03 18:59:21 +00:00
|
|
|
func TestGenerateEnrollmentProfileMobileConfig(t *testing.T) {
|
|
|
|
// SCEP challenge should be escaped for XML
|
2023-03-13 13:33:32 +00:00
|
|
|
b, err := apple_mdm.GenerateEnrollmentProfileMobileconfig("foo", "https://example.com", "foo&bar", "topic")
|
2023-03-03 18:59:21 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, string(b), "foo&bar")
|
|
|
|
}
|
|
|
|
|
2023-04-04 20:09:20 +00:00
|
|
|
func TestEnsureFleetdConfig(t *testing.T) {
|
|
|
|
testError := errors.New("test error")
|
|
|
|
testURL := "https://example.com"
|
|
|
|
testTeamName := "test-team"
|
|
|
|
logger := kitlog.NewNopLogger()
|
|
|
|
|
|
|
|
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 := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
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 := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
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.AppleBMDefaultTeam = 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 {
|
|
|
|
require.Len(t, ps, len(secrets))
|
|
|
|
for i, p := range ps {
|
|
|
|
require.Contains(t, string(p.Mobileconfig), testURL)
|
|
|
|
require.Contains(t, string(p.Mobileconfig), secrets[i].Secret)
|
|
|
|
require.Equal(t, mobileconfig.FleetdConfigPayloadIdentifier, p.Identifier)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
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.AppleBMDefaultTeam = 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 {
|
|
|
|
require.Len(t, ps, len(secrets))
|
|
|
|
for i, p := range ps {
|
|
|
|
require.Contains(t, string(p.Mobileconfig), testURL)
|
|
|
|
require.Contains(t, string(p.Mobileconfig), secrets[i].Secret)
|
|
|
|
require.Equal(t, mobileconfig.FleetdConfigPayloadIdentifier, p.Identifier)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
err := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
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 := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
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 := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
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 := ensureFleetdConfig(ctx, ds, logger)
|
|
|
|
require.ErrorIs(t, err, testError)
|
|
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
|
|
require.True(t, ds.AggregateEnrollSecretPerTeamFuncInvoked)
|
|
|
|
require.True(t, ds.BulkUpsertMDMAppleConfigProfilesFuncInvoked)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-04-25 13:36:01 +00:00
|
|
|
func TestMDMAppleSetupAssistant(t *testing.T) {
|
2023-05-10 20:22:08 +00:00
|
|
|
svc, ctx, ds := setupAppleMDMService(t, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
2023-04-25 13:36:01 +00:00
|
|
|
|
|
|
|
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
|
|
return nil
|
|
|
|
}
|
2023-05-15 18:06:09 +00:00
|
|
|
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
|
|
|
|
return j, nil
|
|
|
|
}
|
2023-04-25 13:36:01 +00:00
|
|
|
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
|
|
|
|
}
|
2023-04-26 14:37:03 +00:00
|
|
|
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
|
|
return &fleet.Team{ID: id}, nil
|
|
|
|
}
|
2023-04-25 13:36:01 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-15 18:01:44 +00:00
|
|
|
func mobileconfigForTest(name, identifier 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.New().String()))
|
|
|
|
}
|
2023-03-17 21:52:30 +00:00
|
|
|
|
|
|
|
func mobileconfigForTestWithContent(name, identifier, inneridentifier, innertype 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>
|
|
|
|
<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>
|
|
|
|
`, name+".inner", inneridentifier, innertype, name, identifier, uuid.New().String()))
|
|
|
|
}
|