<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves # Raised by the frontend while building the Edit modal: there was no way to clear a declaration's custom activation. An absent `activation` field meant "keep it" on a labels-only edit but "delete it" when the profile contents were replaced, so clearing wasn't expressible and an ordinary content edit silently dropped the activation. The field is now three-state: | Request | Result | |---|---| | no `activation` key | stored activation left alone | | `activation` as an empty value | removed | | `activation` as a file | replaced | Multipart has no null, so an empty value stands in for one. Note this changes one existing behaviour: replacing a profile's contents without sending an activation used to delete it, and now preserves it. Removal has to be explicit. Anything ambiguous is rejected rather than guessed at, since every ambiguous form would otherwise resolve to deleting the stored activation: | Request | Result | |---|---| | `activation` as a nonempty value | 422 — more likely a malformed upload than a request to delete | | `activation` as a zero-byte file | 422 — a failed upload shouldn't delete anything | | `activation` sent as both a file and a value | 422 — one says replace, the other says remove | The unsupported-profile check also keys on the field being present rather than on it carrying content, so clearing an activation on a Windows, Android or mobileconfig profile is rejected instead of quietly succeeding. On the datastore side, `SetOrUpdateMDMAppleDeclaration` now takes an explicit action (`MDMAppleActivationKeep` / `MDMAppleActivationApply`) instead of inferring intent from the struct. The write is a full replace, so "keep" has to be stated — otherwise preserving the activation would mean reading it back and handing it to the write, which also risked dropping its Fleet variable associations. As a side effect the OS updates cron no longer fires a DELETE for an activation it never had. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [ ] QA'd all new/changed functionality manually Integration test covers all three states end to end through the multipart decoder, plus service-level tests for preserve and explicit removal. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Apple MDM declaration updates now support preserving, replacing, or explicitly removing activation settings. * Omitted activation fields leave existing settings unchanged, while empty fields remove them. * Apple OS update declarations retain activation settings by default. * **Bug Fixes** * Labels-only updates no longer unintentionally carry forward activation data. * Invalid, empty, or conflicting activation uploads now receive clear validation errors. * Unsupported profile types now reject activation updates. * **Tests** * Added coverage for activation preservation, replacement, removal, and integration scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
649 lines
26 KiB
Go
649 lines
26 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/WatchBeam/clock"
|
|
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql/mysqltest"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
|
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
|
|
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/fleetdm/fleet/v4/server/worker"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Service, context.Context) {
|
|
ds := new(mock.Store)
|
|
lic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
|
ctx := license.NewContext(context.Background(), lic)
|
|
|
|
logger := slog.New(slog.DiscardHandler)
|
|
fleetConfig := config.FleetConfig{
|
|
MDM: config.MDMConfig{
|
|
AppleSCEPCertBytes: eeservice.TestCert,
|
|
AppleSCEPKeyBytes: eeservice.TestKey,
|
|
},
|
|
Server: config.ServerConfig{
|
|
PrivateKey: "foo",
|
|
},
|
|
}
|
|
depStorage := &nanodep_mock.Storage{}
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/server/devices"):
|
|
_, err := w.Write([]byte("{}"))
|
|
require.NoError(t, err)
|
|
case strings.Contains(r.URL.Path, "/session"):
|
|
_, err := w.Write([]byte(`{"auth_session_token": "yoo"}`))
|
|
require.NoError(t, err)
|
|
case strings.Contains(r.URL.Path, "/profile"):
|
|
_, err := w.Write([]byte(`{"profile_uuid": "profile123"}`))
|
|
require.NoError(t, err)
|
|
}
|
|
}))
|
|
depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) {
|
|
return &nanodep_client.Config{
|
|
BaseURL: ts.URL,
|
|
}, nil
|
|
}
|
|
depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
|
|
return &nanodep_client.OAuth1Tokens{}, nil
|
|
}
|
|
t.Cleanup(func() { ts.Close() })
|
|
|
|
freeSvc, err := service.NewService(
|
|
ctx,
|
|
ds,
|
|
nil,
|
|
nil,
|
|
logger,
|
|
nil,
|
|
fleetConfig,
|
|
nil,
|
|
clock.C,
|
|
nil,
|
|
nil,
|
|
ds,
|
|
nil,
|
|
&fleet.NoOpGeoIP{},
|
|
nil,
|
|
depStorage,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
// Using a noop activity service since this test does not currently verify activity creation.
|
|
freeSvc.SetActivityService(&mock.MockActivityService{
|
|
NewActivityFunc: mock.NoopNewActivityFunc,
|
|
})
|
|
svc, err := eeservice.NewService(
|
|
freeSvc,
|
|
ds,
|
|
logger,
|
|
fleetConfig,
|
|
nil,
|
|
clock.C,
|
|
depStorage,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ds, svc, ctx
|
|
}
|
|
|
|
func TestGetOrCreatePreassignTeam(t *testing.T) {
|
|
ds, svc, ctx := setupMockDatastorePremiumService(t)
|
|
|
|
ssoSettings := fleet.SSOProviderSettings{
|
|
EntityID: "foo",
|
|
MetadataURL: "https://example.com/metadata.xml",
|
|
}
|
|
appConfig := &fleet.AppConfig{MDM: fleet.MDM{
|
|
EnabledAndConfigured: true,
|
|
EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: ssoSettings},
|
|
MacOSSetup: fleet.MacOSSetup{
|
|
BootstrapPackage: optjson.SetString("https://example.com/bootstrap.pkg"),
|
|
EnableEndUserAuthentication: true,
|
|
EnableReleaseDeviceManually: optjson.SetBool(true),
|
|
},
|
|
}}
|
|
preassignGroups := []string{"one", "three"}
|
|
|
|
// initialize team store with team one and two already created, one matches
|
|
// preassign group [0], two does not match any preassign group
|
|
team1 := &fleet.Team{
|
|
ID: 1,
|
|
Name: preassignGroups[0],
|
|
}
|
|
team2 := &fleet.Team{
|
|
ID: 2,
|
|
Name: "two",
|
|
Config: fleet.TeamConfig{
|
|
MDM: fleet.TeamMDM{
|
|
MacOSSetup: fleet.MacOSSetup{MacOSSetupAssistant: optjson.SetString("foo/bar")},
|
|
},
|
|
},
|
|
}
|
|
teamStore := map[uint]*fleet.Team{1: team1, 2: team2}
|
|
|
|
resetInvoked := func() {
|
|
ds.TeamByNameFuncInvoked = false
|
|
ds.NewTeamFuncInvoked = false
|
|
ds.SaveTeamFuncInvoked = false
|
|
ds.NewMDMAppleConfigProfileFuncInvoked = false
|
|
ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked = false
|
|
ds.AppConfigFuncInvoked = false
|
|
ds.NewJobFuncInvoked = false
|
|
ds.GetMDMAppleSetupAssistantFuncInvoked = false
|
|
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
|
|
ds.LabelIDsByNameFuncInvoked = false
|
|
ds.SetOrUpdateMDMAppleDeclarationFuncInvoked = false
|
|
ds.BulkSetPendingMDMHostProfilesFuncInvoked = false
|
|
ds.GetAllMDMConfigAssetsByNameFuncInvoked = false
|
|
}
|
|
setupDS := func(t *testing.T) {
|
|
resetInvoked()
|
|
|
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
|
return appConfig, nil
|
|
}
|
|
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
|
for _, team := range teamStore {
|
|
if team.Name == name {
|
|
return team, nil
|
|
}
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, &eeservice.NotFoundError{})
|
|
}
|
|
ds.TeamConflictsWithNameFunc = func(ctx context.Context, name string, excludeID uint) (*fleet.Team, error) {
|
|
for _, team := range teamStore {
|
|
if team.ID == excludeID {
|
|
continue
|
|
}
|
|
if strings.EqualFold(team.Name, name) {
|
|
return team, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
|
tm, ok := teamStore[id]
|
|
if !ok {
|
|
return nil, errors.New("team id not found")
|
|
}
|
|
if id != tm.ID {
|
|
// sanity chec
|
|
return nil, errors.New("team id mismatch")
|
|
}
|
|
return tm, nil
|
|
}
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
ds.CopyDefaultMDMAppleBootstrapPackageFunc = func(ctx context.Context, ac *fleet.AppConfig, toTeamID uint) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
|
require.Len(t, names, 1)
|
|
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
|
|
return map[string]uint{names[0]: 1}, nil
|
|
}
|
|
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName, activationAction fleet.MDMAppleActivationAction) (*fleet.MDMAppleDeclaration, error) {
|
|
declaration.DeclarationUUID = uuid.NewString()
|
|
return declaration, nil
|
|
}
|
|
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
|
) (updates fleet.MDMProfilesUpdates, err error) {
|
|
return fleet.MDMProfilesUpdates{}, nil
|
|
}
|
|
apnsCert, apnsKey, err := mysqltest.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
|
|
require.NoError(t, err)
|
|
certPEM, keyPEM, tokenBytes, err := mysqltest.GenerateTestABMAssets(t)
|
|
require.NoError(t, err)
|
|
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
|
|
_ sqlx.QueryerContext,
|
|
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
|
|
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
|
|
fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM},
|
|
fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM},
|
|
fleet.MDMAssetABMTokenDeprecated: {Name: fleet.MDMAssetABMTokenDeprecated, Value: tokenBytes},
|
|
fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: apnsCert},
|
|
fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: apnsKey},
|
|
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: certPEM},
|
|
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
|
|
}, nil
|
|
}
|
|
|
|
ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
|
|
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
|
|
}
|
|
ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
|
|
return []string{"foobar"}, nil
|
|
}
|
|
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
|
|
return []*fleet.ABMToken{{ID: 1}}, nil
|
|
}
|
|
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
|
|
return 0, nil
|
|
}
|
|
ds.SetABMTokenInvalidForOrgNameFunc = func(ctx context.Context, orgName string, invalid bool) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.IsABMTokenInvalidForOrgNameFunc = func(ctx context.Context, orgName string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
|
|
return nil, &eeservice.NotFoundError{}
|
|
}
|
|
}
|
|
|
|
authzCtx := &authz_ctx.AuthorizationContext{}
|
|
ctx = authz_ctx.NewContext(ctx, authzCtx)
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: test.UserAdmin})
|
|
actx, _ := authz_ctx.FromContext(ctx)
|
|
actx.SetChecked()
|
|
|
|
t.Run("get preassign team", func(t *testing.T) {
|
|
setupDS(t)
|
|
|
|
// preasign group corresponds to existing team so simply get it
|
|
team, err := svc.GetOrCreatePreassignTeam(ctx, preassignGroups[0:1])
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint(1), team.ID)
|
|
require.Equal(t, preassignGroups[0], team.Name)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.False(t, ds.NewTeamFuncInvoked)
|
|
require.False(t, ds.SaveTeamFuncInvoked)
|
|
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.False(t, ds.AppConfigFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
resetInvoked()
|
|
})
|
|
|
|
t.Run("create preassign team", func(t *testing.T) {
|
|
setupDS(t)
|
|
|
|
lastTeamID := uint(0)
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
for _, tm := range teamStore {
|
|
if tm.Name == team.Name {
|
|
return nil, errors.New("team name already exists")
|
|
}
|
|
}
|
|
id := uint(len(teamStore) + 1) //nolint:gosec // dismiss G115
|
|
_, ok := teamStore[id]
|
|
require.False(t, ok) // sanity check
|
|
team.ID = id
|
|
teamStore[id] = team
|
|
lastTeamID = id
|
|
return team, nil
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
tm, ok := teamStore[team.ID]
|
|
if !ok {
|
|
return nil, errors.New("invalid team id")
|
|
}
|
|
require.Equal(t, tm.ID, team.ID) // sanity check
|
|
require.Equal(t, tm.Name, team.Name) // sanity check
|
|
// NOTE: BootstrapPackage gets set by CopyDefaultMDMAppleBootstrapPackage
|
|
// require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // set to default
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant, team.Config.MDM.MacOSSetup.MacOSSetupAssistant) // set to default
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // set to default
|
|
teamStore[tm.ID] = team
|
|
return team, nil
|
|
}
|
|
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
|
require.Equal(t, lastTeamID, *profile.TeamID)
|
|
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profile.Identifier)
|
|
return &profile, nil
|
|
}
|
|
ds.DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc = func(ctx context.Context, teamID *uint, profileIdentifier string) error {
|
|
require.Equal(t, lastTeamID, *teamID)
|
|
require.Equal(t, mobileconfig.FleetFileVaultPayloadIdentifier, profileIdentifier)
|
|
return nil
|
|
}
|
|
ds.CopyDefaultMDMAppleBootstrapPackageFunc = func(ctx context.Context, ac *fleet.AppConfig, toTeamID uint) error {
|
|
require.Equal(t, lastTeamID, toTeamID)
|
|
require.NotNil(t, ac)
|
|
require.Equal(t, "https://example.com/bootstrap.pkg", ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
teamStore[toTeamID].Config.MDM.MacOSSetup.BootstrapPackage = optjson.SetString(ac.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
return nil
|
|
}
|
|
var jobTask string
|
|
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
|
// first task is UpdateProfile, next is ProfileChanged (when setup assistant is set)
|
|
if jobTask == "" {
|
|
jobTask = string(worker.MacosSetupAssistantUpdateProfile)
|
|
} else {
|
|
jobTask = string(worker.MacosSetupAssistantProfileChanged)
|
|
}
|
|
wantArgs, err := json.Marshal(map[string]interface{}{
|
|
"task": jobTask,
|
|
"team_id": lastTeamID,
|
|
})
|
|
require.NoError(t, err)
|
|
wantJob := &fleet.Job{
|
|
Name: "macos_setup_assistant",
|
|
Args: (*json.RawMessage)(&wantArgs),
|
|
State: fleet.JobStateQueued,
|
|
}
|
|
require.Equal(t, wantJob.Name, job.Name)
|
|
require.Equal(t, string(*wantJob.Args), string(*job.Args))
|
|
require.Equal(t, wantJob.State, job.State)
|
|
return job, nil
|
|
}
|
|
setupAsstByTeam := make(map[uint]*fleet.MDMAppleSetupAssistant)
|
|
globalSetupAsst := &fleet.MDMAppleSetupAssistant{
|
|
ID: 15,
|
|
TeamID: nil,
|
|
Name: "test asst",
|
|
Profile: json.RawMessage(`{"foo": "bar"}`),
|
|
}
|
|
setupAsstByTeam[0] = globalSetupAsst
|
|
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
|
var tmID uint
|
|
if teamID != nil {
|
|
tmID = *teamID
|
|
}
|
|
asst := setupAsstByTeam[tmID]
|
|
if asst == nil {
|
|
return nil, &eeservice.NotFoundError{}
|
|
}
|
|
return asst, nil
|
|
}
|
|
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
|
|
require.Equal(t, globalSetupAsst.Name, asst.Name)
|
|
require.JSONEq(t, string(globalSetupAsst.Profile), string(asst.Profile))
|
|
require.NotNil(t, asst.TeamID)
|
|
require.EqualValues(t, lastTeamID, *asst.TeamID)
|
|
setupAsstByTeam[*asst.TeamID] = asst
|
|
return asst, nil
|
|
}
|
|
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
// new team ("one - three") is created with bootstrap package and end user auth based on app config
|
|
team, err := svc.GetOrCreatePreassignTeam(ctx, preassignGroups)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint(3), team.ID)
|
|
require.Equal(t, eeservice.TeamNameFromPreassignGroups(preassignGroups), team.Name)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.True(t, ds.NewTeamFuncInvoked)
|
|
require.True(t, ds.SaveTeamFuncInvoked)
|
|
require.True(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.True(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
|
require.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
|
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant.Value, team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
|
|
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
|
require.True(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
|
require.True(t, ds.NewJobFuncInvoked)
|
|
resetInvoked()
|
|
|
|
jobTask = ""
|
|
|
|
// when called again, simply get the previously created team
|
|
team, err = svc.GetOrCreatePreassignTeam(ctx, preassignGroups)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint(3), team.ID)
|
|
require.Equal(t, eeservice.TeamNameFromPreassignGroups(preassignGroups), team.Name)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.False(t, ds.NewTeamFuncInvoked)
|
|
require.False(t, ds.SaveTeamFuncInvoked)
|
|
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.False(t, ds.AppConfigFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
require.False(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
|
require.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
|
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.MacOSSetupAssistant.Value, team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
|
|
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
|
require.True(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
|
resetInvoked()
|
|
|
|
jobTask = ""
|
|
|
|
// when a custom setup assistant is not set for "no team", we don't create
|
|
// a custom setup assistant
|
|
setupAsstByTeam[0] = nil
|
|
|
|
preassignGrousWithFoo := preassignGroups
|
|
preassignGrousWithFoo = append(preassignGrousWithFoo, "foo")
|
|
team, err = svc.GetOrCreatePreassignTeam(ctx, preassignGrousWithFoo)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint(4), team.ID)
|
|
require.Equal(t, eeservice.TeamNameFromPreassignGroups(preassignGrousWithFoo), team.Name)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.True(t, ds.NewTeamFuncInvoked)
|
|
require.True(t, ds.SaveTeamFuncInvoked)
|
|
require.True(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.True(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
|
|
require.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
|
|
require.NotEmpty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.BootstrapPackage.Value, team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
|
|
require.True(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableEndUserAuthentication, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
|
|
require.True(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
|
require.Equal(t, appConfig.MDM.MacOSSetup.EnableReleaseDeviceManually.Value, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
|
|
resetInvoked()
|
|
})
|
|
|
|
t.Run("modify team via apply team spec", func(t *testing.T) {
|
|
setupDS(t)
|
|
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
tm, ok := teamStore[team.ID]
|
|
if !ok {
|
|
return nil, errors.New("invalid team id")
|
|
}
|
|
require.Equal(t, tm.ID, team.ID) // sanity check
|
|
require.Equal(t, tm.Name, team.Name) // sanity check
|
|
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not modified
|
|
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not modified
|
|
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not modified
|
|
return teamStore[tm.ID], nil
|
|
}
|
|
|
|
// apply team spec does not apply defaults
|
|
spec := &fleet.TeamSpec{
|
|
Name: team2.Name,
|
|
}
|
|
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplyTeamSpecOptions{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SaveTeamFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.False(t, ds.NewTeamFuncInvoked)
|
|
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
resetInvoked()
|
|
})
|
|
|
|
t.Run("new team", func(t *testing.T) {
|
|
setupDS(t)
|
|
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
for _, tm := range teamStore {
|
|
if tm.Name == team.Name {
|
|
return nil, errors.New("team name already exists")
|
|
}
|
|
}
|
|
id := uint(len(teamStore) + 1) //nolint:gosec // dismiss G115
|
|
_, ok := teamStore[id]
|
|
require.False(t, ok) // sanity check
|
|
require.Equal(t, "new team", team.Name)
|
|
require.Equal(t, "new description", team.Description)
|
|
require.False(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
|
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
|
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not set
|
|
team.ID = id
|
|
teamStore[id] = team
|
|
return team, nil
|
|
}
|
|
|
|
// new team does not apply defaults
|
|
_, err := svc.NewTeam(ctx, fleet.TeamPayload{
|
|
Name: ptr.String("new team"),
|
|
Description: ptr.String("new description"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.NewTeamFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.False(t, ds.TeamByNameFuncInvoked)
|
|
require.False(t, ds.SaveTeamFuncInvoked)
|
|
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
resetInvoked()
|
|
})
|
|
|
|
t.Run("apply team spec", func(t *testing.T) {
|
|
setupDS(t)
|
|
|
|
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
for _, tm := range teamStore {
|
|
if tm.Name == team.Name {
|
|
return nil, errors.New("team name already exists")
|
|
}
|
|
}
|
|
id := uint(len(teamStore) + 1) //nolint:gosec // dismiss G115
|
|
_, ok := teamStore[id]
|
|
require.False(t, ok) // sanity check
|
|
require.Equal(t, "new team spec", team.Name) // set
|
|
require.Equal(t, "12.0", team.Config.MDM.MacOSUpdates.MinimumVersion.Value) // set
|
|
require.Equal(t, "2024-01-01", team.Config.MDM.MacOSUpdates.Deadline.Value) // set
|
|
require.False(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
|
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
|
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not set
|
|
team.ID = id
|
|
teamStore[id] = team
|
|
return team, nil
|
|
}
|
|
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
tm, ok := teamStore[team.ID]
|
|
if !ok {
|
|
return nil, errors.New("invalid team id")
|
|
}
|
|
require.Equal(t, tm.ID, team.ID) // sanity check
|
|
require.Equal(t, tm.Name, team.Name) // sanity check
|
|
require.Equal(t, "12.0", team.Config.MDM.MacOSUpdates.MinimumVersion.Value) // unchanged
|
|
require.Equal(t, "2025-01-01", team.Config.MDM.MacOSUpdates.Deadline.Value) // modified
|
|
require.Empty(t, team.Config.MDM.MacOSSetup.EnableEndUserAuthentication) // not set
|
|
require.Empty(t, team.Config.MDM.MacOSSetup.BootstrapPackage.Value) // not set
|
|
require.False(t, team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value) // not set
|
|
|
|
return teamStore[tm.ID], nil
|
|
}
|
|
|
|
spec := &fleet.TeamSpec{
|
|
Name: "new team spec",
|
|
MDM: fleet.TeamSpecMDM{
|
|
MacOSUpdates: fleet.AppleOSUpdateSettings{
|
|
MinimumVersion: optjson.SetString("12.0"),
|
|
Deadline: optjson.SetString("2024-01-01"),
|
|
},
|
|
},
|
|
}
|
|
|
|
// apply team spec creates new team without defaults
|
|
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplyTeamSpecOptions{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.NewTeamFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.False(t, ds.SaveTeamFuncInvoked)
|
|
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
resetInvoked()
|
|
|
|
// apply team spec edits existing team without applying defaults
|
|
spec.MDM.MacOSUpdates.Deadline = optjson.SetString("2025-01-01")
|
|
_, err = svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{spec}, fleet.ApplyTeamSpecOptions{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SaveTeamFuncInvoked)
|
|
require.True(t, ds.AppConfigFuncInvoked)
|
|
require.True(t, ds.TeamByNameFuncInvoked)
|
|
require.False(t, ds.NewTeamFuncInvoked)
|
|
require.False(t, ds.NewMDMAppleConfigProfileFuncInvoked)
|
|
require.False(t, ds.CopyDefaultMDMAppleBootstrapPackageFuncInvoked)
|
|
require.False(t, ds.NewJobFuncInvoked)
|
|
resetInvoked()
|
|
})
|
|
}
|