Files
Rajendra Kadam 25cfac309c Let an edit clear a declaration's activation (#50711)
<!-- 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 -->
2026-08-07 15:36:31 +05:30

4555 lines
142 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fleetctl
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
mdmtest "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
"github.com/fleetdm/fleet/v4/server/mock"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/platform/logging"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
var testSAMLIDPMetadataURL = getTestSAMLIDPMetadataURL()
func getTestSAMLIDPMetadataURL() string {
if port := os.Getenv("FLEET_SAML_IDP_HTTP_PORT"); port != "" {
return "http://localhost:" + port + "/simplesaml/saml2/idp/metadata.php"
}
return "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
}
var userRoleSpecList = []*fleet.User{
{
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Now()},
},
ID: 42,
Name: "Test Name admin1@example.com",
Email: "admin1@example.com",
GlobalRole: ptr.String(fleet.RoleAdmin),
},
{
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Now()},
},
ID: 23,
Name: "Test Name2 admin2@example.com",
Email: "admin2@example.com",
GlobalRole: nil,
Teams: []fleet.UserTeam{},
},
}
func TestApplyUserRoles(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "team1",
}, nil
}
ds.SaveUsersFunc = func(ctx context.Context, users []*fleet.User) error {
for _, u := range users {
switch u.Email {
case "admin1@example.com":
userRoleList[0] = u
case "admin2@example.com":
userRoleList[1] = u
}
}
return nil
}
tmpFile, err := os.CreateTemp(os.TempDir(), "*.yml")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(`
---
apiVersion: v1
kind: user_roles
spec:
roles:
admin1@example.com:
global_role: admin
fleets: null
admin2@example.com:
global_role: null
fleets:
- role: maintainer
fleet: team1
`)
require.NoError(t, err)
assert.Equal(t, "[+] applied user roles\n", runAppForTest(t, []string{"apply", "-f", tmpFile.Name()}))
require.Len(t, userRoleSpecList[1].Teams, 1)
assert.Equal(t, fleet.RoleMaintainer, userRoleSpecList[1].Teams[0].Role)
}
func TestApplyUserRolesDeprecated(t *testing.T) {
t.Setenv("FLEET_ENABLE_LOG_TOPICS", logging.DeprecatedFieldTopic)
_, ds := testing_utils.RunServerWithMockedDS(t)
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "team1",
}, nil
}
ds.SaveUsersFunc = func(ctx context.Context, users []*fleet.User) error {
for _, u := range users {
switch u.Email {
case "admin1@example.com":
userRoleList[0] = u
case "admin2@example.com":
userRoleList[1] = u
}
}
return nil
}
tmpFile, err := os.CreateTemp(os.TempDir(), "*.yml")
require.NoError(t, err)
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(`
---
apiVersion: v1
kind: user_roles
spec:
roles:
admin1@example.com:
global_role: admin
teams: null
admin2@example.com:
global_role: null
teams:
- role: maintainer
team: team1
`)
require.NoError(t, err)
expected := "[!] In user_roles: `team` is deprecated, please use `fleet` instead.\n[!] In user_roles: `teams` is deprecated, please use `fleets` instead.\n[+] applied user roles\n"
assert.Equal(t, expected, runAppForTest(t, []string{"apply", "-f", tmpFile.Name()}))
require.Len(t, userRoleSpecList[1].Teams, 1)
assert.Equal(t, fleet.RoleMaintainer, userRoleSpecList[1].Teams[0].Role)
_, err = tmpFile.WriteString(`
---
apiVersion: v1
kind: user_roles
spec:
roles:
admin1@example.com:
global_role: admin
teams: null
admin2@example.com:
global_role: null
teams:
- role: maintainer
team: team1
fleet: team1
`)
require.NoError(t, err)
runAppCheckErr(t, []string{"apply", "-f", tmpFile.Name()}, "in user_roles spec: Conflicting field names: cannot specify both `team` (deprecated) and `fleet` in the same request")
}
func TestApplyTeamSpecs(t *testing.T) {
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license})
// Mock Apple GDMF API (required for validating OS update minimum version settings)
mdmtest.StartNewAppleGDMFTestServer(t)
teamsByName := map[string]*fleet.Team{
"team1": {
ID: 42,
Name: "team1",
Description: "team1 description",
},
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
team, ok := teamsByName[name]
if !ok {
return nil, &notFoundError{}
}
return team, nil
}
i := 1
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
team.ID = uint(i) //nolint:gosec // dismiss G115
i++
teamsByName[team.Name] = team
return team, nil
}
agentOpts := json.RawMessage(`{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}}`)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
AgentOptions: &agentOpts,
MDM: fleet.MDM{EnabledAndConfigured: true},
Integrations: fleet.Integrations{
GoogleCalendar: []*fleet.GoogleCalendarIntegration{{}},
},
}, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
teamsByName[team.Name] = team
return team, nil
}
enrolledSecretsCalled := make(map[uint][]*fleet.EnrollSecret)
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledSecretsCalled[*teamID] = secrets
return nil
}
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
return nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
require.Len(t, names, 1)
switch names[0] {
case fleet.BuiltinLabelMacOS14Plus:
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
case fleet.BuiltinLabelIOS:
return map[string]uint{fleet.BuiltinLabelIOS: 2}, nil
case fleet.BuiltinLabelIPadOS:
return map[string]uint{fleet.BuiltinLabelIPadOS: 3}, nil
default:
return nil, &notFoundError{}
}
}
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.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return nil, &notFoundError{}
}
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
ds.HasWindowsUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
filename := writeTmpYml(t, `
---
apiVersion: v1
kind: fleet
spec:
fleet:
name: team2
---
apiVersion: v1
kind: fleet
spec:
fleet:
agent_options:
config:
views:
foo: bar
name: team1
secrets:
- secret: AAA
mdm:
macos_updates:
minimum_version: 14.6.1
deadline: 2011-03-01
update_new_hosts: true
`)
newAgentOpts := json.RawMessage(`{"config":{"views":{"foo":"bar"}}}`)
newMDMSettings := fleet.TeamMDM{
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2011-03-01"),
UpdateNewHosts: optjson.SetBool(true),
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}
require.Equal(t, "[+] applied 2 fleets\n", runAppForTest(t, []string{"apply", "-f", filename}))
assert.JSONEq(t, string(agentOpts), string(*teamsByName["team2"].Config.AgentOptions))
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "AAA"}}, enrolledSecretsCalled[uint(42)])
assert.Equal(t, fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}, teamsByName["team2"].Config.MDM)
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
ds.ApplyEnrollSecretsFuncInvoked = false
// add windows updates settings to team1
filename = writeTmpYml(t, `
---
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
mdm:
windows_updates:
deadline_days: 5
grace_period_days: 1
`)
require.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", filename}))
newMDMSettings = fleet.TeamMDM{
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2011-03-01"),
UpdateNewHosts: optjson.SetBool(true),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(5),
GracePeriodDays: optjson.SetInt(1),
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
mobileCfgPath := writeTmpMobileconfig(t, "N1")
filename = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
mdm:
macos_settings:
custom_settings:
- %s
`, mobileCfgPath))
newMDMSettings = fleet.TeamMDM{
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2011-03-01"),
UpdateNewHosts: optjson.SetBool(true),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(5),
GracePeriodDays: optjson.SetInt(1),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileCfgPath}},
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", filename}), "[+] applied 1 fleet\n")
// enroll secret not provided, so left unchanged
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "AAA"}}, enrolledSecretsCalled[uint(42)])
assert.False(t, ds.ApplyEnrollSecretsFuncInvoked)
// agent options not provided, so left unchanged
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
// macos updates options not provided, left unchanged, and macos custom settings added
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
filename = writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
fleet:
agent_options:
config:
views:
foo: qux
name: team1
mdm:
macos_updates:
minimum_version: 14.6.1
deadline: 1992-03-01
ios_updates:
minimum_version: 17.6.1
deadline: 1993-04-02
ipados_updates:
minimum_version: 17.6.1
deadline: 1994-05-03
secrets:
- secret: BBB
`)
newMDMSettings = fleet.TeamMDM{
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("1992-03-01"),
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("1993-04-02"),
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("17.6.1"),
Deadline: optjson.SetString("1994-05-03"),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(5),
GracePeriodDays: optjson.SetInt(1),
},
MacOSSettings: fleet.MacOSSettings{ // macos settings not provided, so not cleared
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileCfgPath}},
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}
newAgentOpts = json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)
require.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", filename}))
assert.JSONEq(t, string(newAgentOpts), string(*teamsByName["team1"].Config.AgentOptions))
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, enrolledSecretsCalled[uint(42)])
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
filename = writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
fleet:
agent_options:
name: team1
mdm:
macos_updates:
apple_settings:
`)
require.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", filename}))
// agent options provided but empty, clears the value
assert.Nil(t, teamsByName["team1"].Config.AgentOptions)
// macos settings and updates still the same (not cleared) because only the
// top-level key is provided.
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
// enroll secret not cleared since not provided
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, enrolledSecretsCalled[uint(42)])
filename = writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
mdm:
macos_updates:
minimum_version:
`)
// fails: minimum_version provided empty, but deadline not provided
_, err := runAppNoChecks([]string{"apply", "-f", filename})
require.ErrorContains(t, err, "deadline is required when minimum_version is provided")
filename = writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
mdm:
macos_updates:
minimum_version:
deadline:
ios_updates:
minimum_version:
deadline:
ipados_updates:
minimum_version:
deadline:
windows_updates:
deadline_days:
grace_period_days:
macos_settings:
custom_settings:
`)
newMDMSettings = fleet.TeamMDM{
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.String{Set: true},
Deadline: optjson.String{Set: true},
},
IOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.String{Set: true},
Deadline: optjson.String{Set: true},
},
IPadOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.String{Set: true},
Deadline: optjson.String{Set: true},
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.Int{Set: true},
GracePeriodDays: optjson.Int{Set: true},
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{},
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", filename}), "[+] applied 1 fleet\n")
// agent options still cleared
assert.Nil(t, teamsByName["team1"].Config.AgentOptions)
// macos settings and updates are now cleared.
assert.Equal(t, newMDMSettings, teamsByName["team1"].Config.MDM)
// enroll secret not cleared since not provided
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, enrolledSecretsCalled[uint(42)])
// Apply team host_status_webhook
filename = writeTmpYml(
t, `
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
webhook_settings:
host_status_webhook:
days_count: 14
destination_url: https://example.com
enable_host_status_webhook: true
host_percentage: 25
`,
)
require.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", filename}))
// Ensure the webhook settings are applied
assert.Equal(
t, fleet.HostStatusWebhookSettings{
DaysCount: 14,
DestinationURL: "https://example.com",
Enable: true,
HostPercentage: 25,
}, *teamsByName["team1"].Config.WebhookSettings.HostStatusWebhook,
)
assert.Equal(t, fleet.FailingPoliciesWebhookSettings{}, teamsByName["team1"].Config.WebhookSettings.FailingPoliciesWebhook)
// enroll secret not cleared since not provided
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, enrolledSecretsCalled[uint(42)])
// Apply empty webhook settings
filename = writeTmpYml(
t, `
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
webhook_settings:
`,
)
require.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", filename}))
// Ensure the webhook settings have not changed
assert.Equal(
t, fleet.HostStatusWebhookSettings{
DaysCount: 14,
DestinationURL: "https://example.com",
Enable: true,
HostPercentage: 25,
}, *teamsByName["team1"].Config.WebhookSettings.HostStatusWebhook,
)
// Apply calendar integration
filename = writeTmpYml(
t, `
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
integrations:
google_calendar:
enable_calendar_events: true
webhook_url: https://example.com/webhook
`,
)
require.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", filename}))
require.NotNil(t, teamsByName["team1"].Config.Integrations.GoogleCalendar)
assert.Equal(
t, fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: "https://example.com/webhook",
}, *teamsByName["team1"].Config.Integrations.GoogleCalendar,
)
// Apply calendar integration -- invalid webhook destination
filename = writeTmpYml(
t, `
apiVersion: v1
kind: fleet
spec:
fleet:
name: team1
integrations:
google_calendar:
enable_calendar_events: true
webhook_url: bozo
`,
)
_, err = runAppNoChecks([]string{"apply", "-f", filename})
assert.ErrorContains(t, err, "invalid URI for request")
}
func writeTmpYml(t *testing.T, contents string) string {
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(contents)
require.NoError(t, err)
return tmpFile.Name()
}
func writeTmpJSON(t *testing.T, v any) string {
tmpFile, err := os.CreateTemp(t.TempDir(), "*.json")
require.NoError(t, err)
err = json.NewEncoder(tmpFile).Encode(v)
require.NoError(t, err)
return tmpFile.Name()
}
func TestApplyAppConfig(t *testing.T) {
t.Setenv("FLEET_ENABLE_LOG_TOPICS", logging.DeprecatedFieldTopic)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license})
// Mock Apple GDMF API (required for validating OS update minimum version settings)
mdmtest.StartNewAppleGDMFTestServer(t)
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
return &fleet.Team{ID: 123}, nil
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
return nil
}
defaultAgentOpts := json.RawMessage(`{"config":{"foo":"bar"}}`)
savedAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"},
ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
AgentOptions: &defaultAgentOpts,
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return savedAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 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.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}}, nil
}
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
return []*fleet.TeamSummary{{Name: "team1", ID: 1}}, nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{OrganizationName: t.Name()}}, nil
}
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
ds.HasWindowsUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
features:
enable_host_users: false
enable_software_inventory: false
mdm:
apple_bm_default_team: "team1"
macos_updates:
minimum_version: 14.6.1
deadline: 2011-02-01
windows_updates:
deadline_days: 5
grace_period_days: 1
`)
newMDMSettings := fleet.MDM{
DeprecatedAppleBMDefaultTeam: "team1",
WindowsSettings: fleet.WindowsSettings{ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)}},
AppleBMTermsExpired: false,
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2011-02-01"),
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(5),
GracePeriodDays: optjson.SetInt(1),
},
}
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.False(t, savedAppConfig.Features.EnableHostUsers)
assert.False(t, savedAppConfig.Features.EnableSoftwareInventory)
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
// agent options were not modified, since they were not provided
assert.Equal(t, string(defaultAgentOpts), string(*savedAppConfig.AgentOptions))
// Test key rewriting (deprecated -> new)
name = writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
server_settings:
report_cap: 100
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.Equal(t, 100, savedAppConfig.ServerSettings.QueryReportCap)
// Test deprecation warnings
expected := "[!] In config: `query_report_cap` is deprecated, please use `report_cap` instead.\n[+] applied fleet config\n"
name = writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
server_settings:
query_report_cap: 200
`)
assert.Equal(t, expected, runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.Equal(t, 200, savedAppConfig.ServerSettings.QueryReportCap)
name = writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
features:
enable_host_users: true
enable_software_inventory: true
agent_options:
mdm:
macos_updates:
windows_updates:
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.True(t, savedAppConfig.Features.EnableHostUsers)
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
// agent options were cleared, provided but empty
assert.Nil(t, savedAppConfig.AgentOptions)
// MDM settings unchanged, not provided
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
name = writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days:
grace_period_days:
`)
newMDMSettings = fleet.MDM{
DeprecatedAppleBMDefaultTeam: "team1",
WindowsSettings: fleet.WindowsSettings{ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)}},
AppleBMTermsExpired: false,
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2011-02-01"),
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.Int{Set: true},
GracePeriodDays: optjson.Int{Set: true},
},
}
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.Equal(t, newMDMSettings, savedAppConfig.MDM)
}
func TestApplyAppConfigAliasConfict(t *testing.T) {
// Test conflict error
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
server_settings:
query_report_cap: 200
report_cap: 200
`)
runAppCheckErr(t, []string{"apply", "-f", name}, "in config spec: Conflicting field names: cannot specify both `query_report_cap` (deprecated) and `report_cap` in the same request")
}
func TestApplyAppConfigDryRunIssue(t *testing.T) {
// reproduces the bug fixed by https://github.com/fleetdm/fleet/pull/8194
_, ds := testing_utils.RunServerWithMockedDS(t)
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
currentAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return currentAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
currentAppConfig = config
return nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return nil, &notFoundError{}
}
// first, set the default app config's agent options as set after fleetctl setup
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
agent_options:
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
overrides: {}
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
// then, dry-run a valid app config's agent options, which made the original
// app config's agent options invalid JSON (when it shouldn't have modified
// it at all - the issue was in the cached_mysql datastore, it did not clone
// the app config properly).
name = writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
agent_options:
overrides:
platforms:
darwin:
auto_table_construction:
tcc_system_entries:
query: "SELECT service, client, allowed, prompt_count, last_modified FROM access"
path: "/Library/Application Support/com.apple.TCC/TCC.db"
columns:
- "service"
- "client"
- "allowed"
- "prompt_count"
- "last_modified"
`)
assert.Equal(t, "[+] would've applied fleet config\n", runAppForTest(t, []string{"apply", "--dry-run", "-f", name}))
// the saved app config was left unchanged, still equal to the original agent
// options
got := runAppForTest(t, []string{"get", "config"})
assert.Contains(t, got, `agent_options:
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
overrides: {}`)
}
func TestApplyAppConfigUnknownFields(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
var savedAppConfig *fleet.AppConfig
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
features:
enabled_software_inventory: false # typo, correct config is enable_software_inventory
`)
runAppCheckErr(t, []string{"apply", "-f", name},
"applying fleet config: PATCH /api/latest/fleet/config received status 400 Bad Request: unsupported key provided: \"enabled_software_inventory\"",
)
require.Nil(t, savedAppConfig)
}
func TestApplyAppConfigDeprecatedFields(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
}
var savedAppConfig *fleet.AppConfig
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
host_settings:
enable_host_users: false
enable_software_inventory: false
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.False(t, savedAppConfig.Features.EnableHostUsers)
assert.False(t, savedAppConfig.Features.EnableSoftwareInventory)
name = writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
host_settings:
enable_host_users: true
enable_software_inventory: true
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.NotNil(t, savedAppConfig)
assert.True(t, savedAppConfig.Features.EnableHostUsers)
assert.True(t, savedAppConfig.Features.EnableSoftwareInventory)
}
const (
policySpec = `---
apiVersion: v1
kind: policy
spec:
name: Is Gatekeeper enabled on macOS devices?
query: SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;
description: Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.
resolution: "Run the following command in the Terminal app: /usr/sbin/spctl --master-enable"
platform: darwin
fleet: Team1
---
apiVersion: v1
kind: policy
spec:
name: Is disk encryption enabled on Windows devices?
query: SELECT 1 FROM bitlocker_info where protection_status = 1;
description: Checks to make sure that device encryption is enabled on Windows devices.
resolution: "Option 1: Select the Start button. Select Settings > Update & Security > Device encryption. If Device encryption doesn't appear, skip to Option 2. If device encryption is turned off, select Turn on. Option 2: Select the Start button. Under Windows System, select Control Panel. Select System and Security. Under BitLocker Drive Encryption, select Manage BitLocker. Select Turn on BitLocker and then follow the instructions."
platform: windows
---
apiVersion: v1
kind: policy
spec:
name: Is Filevault enabled on macOS devices?
query: SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT “” AND filevault_status = on LIMIT 1;
description: Checks to make sure that the Filevault feature is enabled on macOS devices.
resolution: "Choose Apple menu > System Preferences, then click Security & Privacy. Click the FileVault tab. Click the Lock icon, then enter an administrator name and password. Click Turn On FileVault."
platform: darwin
`
duplicateTeamPolicySpec = `---
apiVersion: v1
kind: policy
spec:
name: Is Gatekeeper enabled on macOS devices?
query: SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;
description: Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.
resolution: "Run the following command in the Terminal app: /usr/sbin/spctl --master-enable"
platform: darwin
fleet: Team1
---
apiVersion: v1
kind: policy
spec:
name: Is Gatekeeper enabled on macOS devices?
query: SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;
description: Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.
resolution: "Run the following command in the Terminal app: /usr/sbin/spctl --master-enable"
platform: darwin
fleet: Team1
`
duplicateGlobalPolicySpec = `---
apiVersion: v1
kind: policy
spec:
name: Is Gatekeeper enabled on macOS devices?
query: SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;
description: Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.
resolution: "Run the following command in the Terminal app: /usr/sbin/spctl --master-enable"
platform: darwin
---
apiVersion: v1
kind: policy
spec:
name: Is Gatekeeper enabled on macOS devices?
query: SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;
description: Checks to make sure that the Gatekeeper feature is enabled on macOS devices. Gatekeeper tries to ensure only trusted software is run on a mac machine.
resolution: "Run the following command in the Terminal app: /usr/sbin/spctl --master-enable"
platform: darwin
`
enrollSecretsSpec = `---
apiVersion: v1
kind: enroll_secret
spec:
secrets:
- secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
- secret: reallyworks
- secret: thissecretwontwork!
`
labelsSpec = `---
apiVersion: v1
kind: label
spec:
name: pending_updates
query: select 1;
platforms:
- darwin
`
manualLabelSpec = `---
apiVersion: v1
kind: label
spec:
name: manual_label
label_membership_type: manual
hosts:
- host1
platforms:
- darwin
`
emptyManualLabelSpec = `---
apiVersion: v1
kind: label
spec:
name: empty_manual_label
label_membership_type: manual
hosts: []
platforms:
- darwin
`
nohostsManualLabelSpec = `---
apiVersion: v1
kind: label
spec:
name: nohost_manual_label
label_membership_type: manual
platforms:
- darwin
`
nullHostsManualLabelSpec = `---
apiVersion: v1
kind: label
spec:
name: nullhost_manual_label
label_membership_type: manual
hosts:
platforms:
- darwin
`
builtinLabelSpec = `---
apiVersion: v1
kind: label
spec:
description: All Ubuntu hosts
hosts: null
id: 8
label_membership_type: dynamic
label_type: builtin
name: Ubuntu Linux
query: select 1 from os_version where platform = 'ubuntu';
`
packsSpec = `---
apiVersion: v1
kind: pack
spec:
name: osquery_monitoring
reports:
- report: osquery_version
name: osquery_version_snapshot
interval: 7200
snapshot: true
- report: osquery_version
name: osquery_version_differential
interval: 7200
`
queriesSpec = `---
apiVersion: v1
kind: report
spec:
description: Retrieves the list of application scheme/protocol-based IPC handlers.
name: app_schemes
query: select * from app_schemes;
`
)
func TestApplyPolicies(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return &fleet.Team{ID: 123}, nil
}
return nil, errors.New("unexpected team name!")
}
name := writeTmpYml(t, policySpec)
assert.Equal(t, "[+] applied 3 policies\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPolicySpecsFuncInvoked)
assert.Len(t, appliedPolicySpecs, 3)
for _, p := range appliedPolicySpecs {
assert.NotEmpty(t, p.Platform)
}
assert.True(t, ds.TeamByNameFuncInvoked)
}
func TestApplyPoliciesValidation(t *testing.T) {
// Team Policy Spec
filename := writeTmpYml(t, duplicateTeamPolicySpec)
errorMsg := `applying policies: policy names must be unique. Please correct policy "Is Gatekeeper enabled on macOS devices?" and try again.`
runAppCheckErr(t, []string{"apply", "-f", filename}, errorMsg)
// Global Policy Spec
filename = writeTmpYml(t, duplicateGlobalPolicySpec)
errorMsg = `applying policies: policy names must be unique. Please correct policy "Is Gatekeeper enabled on macOS devices?" and try again.`
runAppCheckErr(t, []string{"apply", "-f", filename}, errorMsg)
}
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()))
}
func TestApplyAsGitOps(t *testing.T) {
enqueuer := new(mdmmock.MDMAppleStore)
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
// mdm test configuration must be set so that activating windows MDM works.
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
fleetCfg := config.TestConfig()
// Mock Apple DEP API
depStorage := SetupMockDEPStorageAndMockDEPServer(t)
// Mock Apple GDMF API (required for validating OS update minimum version settings)
mdmtest.StartNewAppleGDMFTestServer(t)
config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, "../../../server/service/testdata")
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{
License: license,
MDMStorage: enqueuer,
MDMPusher: testing_utils.MockPusher{},
FleetConfig: &fleetCfg,
DEPStorage: depStorage,
})
gitOps := &fleet.User{
Name: "GitOps",
Password: []byte("p4ssw0rd.123"),
Email: "gitops1@example.com",
GlobalRole: ptr.String(fleet.RoleGitOps),
}
gitOps, err = ds.NewUser(context.Background(), gitOps)
require.NoError(t, err)
ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) {
return &fleet.Session{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
ID: 1,
AccessedAt: time.Now(),
UserID: gitOps.ID,
Key: key,
}, nil
}
ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
return gitOps, nil
}
currentAppConfig := &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{
OrgName: "Fleet",
},
ServerSettings: fleet.ServerSettings{
ServerURL: "https://example.org",
},
MDM: fleet.MDM{
EnabledAndConfigured: true,
},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return currentAppConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
currentAppConfig = config
return nil
}
savedTeam := &fleet.Team{ID: 123}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
if name == "Team1" {
return savedTeam, nil
}
return nil, errors.New("unexpected team name!")
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeam = team
return team, nil
}
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
return savedTeam, nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
assert.False(t, isNew)
assert.Equal(t, uint(123), *teamID)
return true, nil
}
var teamEnrollSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
if teamID == nil || *teamID != 123 {
return fmt.Errorf("unexpected data: %+v", teamID)
}
teamEnrollSecrets = secrets
return nil
}
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
return nil, &notFoundError{}
}
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
return asst, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, &notFoundError{}
}
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
return nil
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
return nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
}
ds.SetAsideLabelsFunc = func(ctx context.Context, notOnTeamID *uint, names []string, user fleet.User) error {
return 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.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
}
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
return 0, nil
}
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.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.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return nil, &notFoundError{}
}
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
ds.HasWindowsUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
// Apply global config.
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
features:
enable_host_users: true
enable_software_inventory: true
agent_options:
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
overrides: {}
`)
// test applying with dry-run flag
assert.Equal(t, "[+] would've applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name, "--dry-run"}))
// test applying for real
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, currentAppConfig.Features.EnableHostUsers)
mobileConfig := mobileconfigForTest("foo", "bar")
mobileConfigPath := filepath.Join(t.TempDir(), "foo.mobileconfig")
err = os.WriteFile(mobileConfigPath, mobileConfig, 0o644)
require.NoError(t, err)
emptySetupAsst := writeTmpJSON(t, map[string]any{})
// Apply global config with custom setting and macos setup assistant, and enable
// Windows MDM.
name = writeTmpYml(t, fmt.Sprintf(`---
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: 14.6.1
deadline: 2020-02-02
windows_updates:
deadline_days: 1
grace_period_days: 0
apple_settings:
configuration_profiles:
- %s
setup_experience:
apple_setup_assistant: %s
windows_enabled_and_configured: true
`, mobileConfigPath, emptySetupAsst))
// first apply with dry-run
assert.Equal(t, "[+] would've applied fleet config\n[+] would've applied MDM profiles\n",
runAppForTest(t, []string{"apply", "-f", name, "--dry-run"}))
// then apply for real
assert.Equal(t, "[+] applied fleet config\n[+] applied MDM profiles\n", runAppForTest(t, []string{"apply", "-f", name}))
// features left untouched, not provided
assert.True(t, currentAppConfig.Features.EnableHostUsers)
assert.Equal(t, fleet.MDM{
EnabledAndConfigured: true,
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2020-02-02"),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(1),
GracePeriodDays: optjson.SetInt(0),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
},
WindowsSettings: fleet.WindowsSettings{ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)}},
WindowsEnabledAndConfigured: true,
}, currentAppConfig.MDM)
// start a server to return the bootstrap package
srv, _ := testing_utils.ServeMDMBootstrapPackage(t, "../../../server/service/testdata/bootstrap-packages/signed.pkg", "signed.pkg")
// Apply global config with bootstrap package
bootstrapURL := srv.URL + "/signed.pkg"
name = writeTmpYml(t, fmt.Sprintf(`---
apiVersion: v1
kind: config
spec:
mdm:
setup_experience:
macos_bootstrap_package: %s
`, bootstrapURL))
// first apply with dry-run
assert.Equal(t, "[+] would've applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name, "--dry-run"}))
// then apply for real
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
// features left untouched, not provided
assert.True(t, currentAppConfig.Features.EnableHostUsers)
// MDM settings left untouched except for the bootstrap package
assert.Equal(t, fleet.MDM{
EnabledAndConfigured: true,
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
BootstrapPackage: optjson.SetString(bootstrapURL),
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("2020-02-02"),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(1),
GracePeriodDays: optjson.SetInt(0),
},
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
},
WindowsSettings: fleet.WindowsSettings{ManagedLocalAccountSettings: fleet.ManagedLocalAccountSettings{Enabled: optjson.SetBool(false)}},
WindowsEnabledAndConfigured: true,
}, currentAppConfig.MDM)
// Apply team config.
name = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: fleet
spec:
team:
agent_options:
config:
views:
foo: qux
name: Team1
mdm:
enable_disk_encryption: false
macos_updates:
minimum_version: 14.6.1
deadline: 1992-03-01
windows_updates:
deadline_days: 0
grace_period_days: 1
macos_settings:
custom_settings:
- %s
secrets:
- secret: BBB
`, mobileConfigPath))
// first apply with dry-run
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", name, "--dry-run"}), "[+] would've applied 1 fleet\n")
// then apply for real
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", name}), "[+] applied 1 fleet\n")
assert.JSONEq(t, string(json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)), string(*savedTeam.Config.AgentOptions))
assert.Equal(t, fleet.TeamMDM{
EnableDiskEncryption: false,
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("1992-03-01"),
},
MacOSSetup: fleet.MacOSSetup{
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(0),
GracePeriodDays: optjson.SetInt(1),
},
}, savedTeam.Config.MDM)
assert.Equal(t, []*fleet.EnrollSecret{{Secret: "BBB"}}, teamEnrollSecrets)
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
assert.True(t, ds.BatchSetMDMProfilesFuncInvoked)
// add macos setup assistant to team
name = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: fleet
spec:
team:
name: Team1
mdm:
macos_setup:
macos_setup_assistant: %s
`, emptySetupAsst))
// first apply with dry-run
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", name, "--dry-run"}), "[+] would've applied 1 fleet\n")
// then apply for real
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", name}), "[+] applied 1 fleet\n")
require.True(t, ds.GetMDMAppleSetupAssistantFuncInvoked)
require.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
require.True(t, ds.NewJobFuncInvoked)
// all left untouched, only setup assistant added
assert.Equal(t, fleet.TeamMDM{
EnableDiskEncryption: false,
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("1992-03-01"),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(0),
GracePeriodDays: optjson.SetInt(1),
},
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}, savedTeam.Config.MDM)
// add bootstrap package to team
name = writeTmpYml(t, fmt.Sprintf(`
apiVersion: v1
kind: fleet
spec:
team:
name: Team1
mdm:
macos_setup:
macos_bootstrap_package: %s
`, bootstrapURL))
// first apply with dry-run
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", name, "--dry-run"}), "[+] would've applied 1 fleet\n")
// then apply for real
assert.Contains(t, runAppForTest(t, []string{"apply", "-f", name}), "[+] applied 1 fleet\n")
// all left untouched, only bootstrap package added
assert.Equal(t, fleet.TeamMDM{
EnableDiskEncryption: false,
MacOSSettings: fleet.MacOSSettings{
CustomSettings: []fleet.MDMProfileSpec{{Path: mobileConfigPath}},
},
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("14.6.1"),
Deadline: optjson.SetString("1992-03-01"),
},
WindowsUpdates: fleet.WindowsUpdates{
DeadlineDays: optjson.SetInt(0),
GracePeriodDays: optjson.SetInt(1),
},
MacOSSetup: fleet.MacOSSetup{
MacOSSetupAssistant: optjson.SetString(emptySetupAsst),
BootstrapPackage: optjson.SetString(bootstrapURL),
EnableReleaseDeviceManually: optjson.SetBool(false),
EnableManagedLocalAccount: optjson.SetBool(false),
EndUserLocalAccountType: optjson.SetString("admin"),
LockEndUserInfo: optjson.SetBool(false),
},
}, savedTeam.Config.MDM)
// Apply policies.
var appliedPolicySpecs []*fleet.PolicySpec
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
appliedPolicySpecs = specs
return nil
}
name = writeTmpYml(t, policySpec)
assert.Equal(t, "[+] applied 3 policies\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPolicySpecsFuncInvoked)
assert.Len(t, appliedPolicySpecs, 3)
for _, p := range appliedPolicySpecs {
assert.NotEmpty(t, p.Platform)
}
assert.True(t, ds.TeamByNameFuncInvoked)
// Apply enroll secrets.
var appliedSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
appliedSecrets = secrets
return nil
}
name = writeTmpYml(t, enrollSecretsSpec)
assert.Equal(t, "[+] applied enroll secrets\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
assert.Len(t, appliedSecrets, 3)
for _, s := range appliedSecrets {
assert.NotEmpty(t, s.Secret)
}
// Apply labels.
var appliedLabels []*fleet.LabelSpec
ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorId *uint) error {
appliedLabels = specs
return nil
}
name = writeTmpYml(t, labelsSpec)
assert.Equal(t, "[+] applied 1 label\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "pending_updates", appliedLabels[0].Name)
assert.Equal(t, "select 1;", appliedLabels[0].Query)
// Apply packs.
var appliedPacks []*fleet.PackSpec
ds.ApplyPackSpecsFunc = func(ctx context.Context, specs []*fleet.PackSpec) error {
appliedPacks = specs
return nil
}
ds.ListPacksFunc = func(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
return nil, nil
}
name = writeTmpYml(t, packsSpec)
assert.Equal(t, "[+] applied 1 pack\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPackSpecsFuncInvoked)
require.Len(t, appliedPacks, 1)
assert.Equal(t, "osquery_monitoring", appliedPacks[0].Name)
require.Len(t, appliedPacks[0].Queries, 2)
// Apply queries.
var appliedQueries []*fleet.Query
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, &notFoundError{}
}
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{}) error {
appliedQueries = queries
return nil
}
name = writeTmpYml(t, queriesSpec)
assert.Equal(t, "[+] applied 1 report\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyQueriesFuncInvoked)
require.Len(t, appliedQueries, 1)
assert.Equal(t, "app_schemes", appliedQueries[0].Name)
assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query)
}
func SetupMockDEPStorageAndMockDEPServer(t *testing.T) *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 := &nanodep_mock.Storage{}
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() })
return depStorage
}
func TestApplyEnrollSecrets(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
var appliedSecrets []*fleet.EnrollSecret
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
appliedSecrets = secrets
return nil
}
name := writeTmpYml(t, enrollSecretsSpec)
assert.Equal(t, "[+] applied enroll secrets\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyEnrollSecretsFuncInvoked)
assert.Len(t, appliedSecrets, 3)
for _, s := range appliedSecrets {
assert.NotEmpty(t, s.Secret)
}
}
func TestApplyLabels(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
var appliedLabels []*fleet.LabelSpec
ds.SetAsideLabelsFunc = func(ctx context.Context, notOnTeamID *uint, names []string, user fleet.User) error {
return nil
}
ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorId *uint) error {
appliedLabels = specs
return nil
}
name := writeTmpYml(t, labelsSpec)
assert.Equal(t, "[+] applied 1 label\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "pending_updates", appliedLabels[0].Name)
assert.Equal(t, "select 1;", appliedLabels[0].Query)
appliedLabels = nil
ds.ApplyLabelSpecsWithAuthorFuncInvoked = false
name = writeTmpYml(t, manualLabelSpec)
assert.Equal(t, "[+] applied 1 label\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "manual_label", appliedLabels[0].Name)
assert.Empty(t, appliedLabels[0].Query)
appliedLabels = nil
ds.ApplyLabelSpecsWithAuthorFuncInvoked = false
name = writeTmpYml(t, emptyManualLabelSpec)
assert.Equal(t, "[+] applied 1 label\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "empty_manual_label", appliedLabels[0].Name)
assert.Empty(t, appliedLabels[0].Query)
appliedLabels = nil
ds.ApplyLabelSpecsWithAuthorFuncInvoked = false
name = writeTmpYml(t, nohostsManualLabelSpec)
assert.Equal(t, "[+] applied 1 label\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "nohost_manual_label", appliedLabels[0].Name)
assert.Nil(t, appliedLabels[0].Hosts)
appliedLabels = nil
ds.ApplyLabelSpecsWithAuthorFuncInvoked = false
name = writeTmpYml(t, nullHostsManualLabelSpec)
assert.Equal(t, "[+] applied 1 label\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
require.Len(t, appliedLabels, 1)
assert.Equal(t, "nullhost_manual_label", appliedLabels[0].Name)
require.NotNil(t, appliedLabels[0].Hosts)
assert.Empty(t, appliedLabels[0].Hosts)
appliedLabels = nil
ds.ApplyLabelSpecsWithAuthorFuncInvoked = false
// Apply built-in label (no changes)
// The label values below should match the spec.
ubuntuLabel := &fleet.Label{
ID: 8,
Name: fleet.BuiltinLabelNameUbuntuLinux,
Query: "select 1 from os_version where platform = 'ubuntu';",
Description: "All Ubuntu hosts",
LabelType: fleet.LabelTypeBuiltIn,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
}
ds.LabelsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]*fleet.Label, error) {
assert.ElementsMatch(t, []string{fleet.BuiltinLabelNameUbuntuLinux}, names)
return map[string]*fleet.Label{
fleet.BuiltinLabelNameUbuntuLinux: ubuntuLabel,
}, nil
}
// Reset invocation flag — earlier sub-cases call LabelsByName as part of
// the regular-label apply flow (used for created/edited activity detection).
ds.LabelsByNameFuncInvoked = false
name = writeTmpYml(t, builtinLabelSpec)
_, err := runAppNoChecks([]string{"apply", "-f", name})
require.Error(t, err)
assert.ErrorContains(t, err, "Cannot import built-in labels. Please remove labels with a label_type of builtin and try again.")
assert.False(t, ds.ApplyLabelSpecsWithAuthorFuncInvoked)
assert.False(t, ds.LabelsByNameFuncInvoked)
}
func TestApplyPacks(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
ds.ListPacksFunc = func(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
return nil, nil
}
var appliedPacks []*fleet.PackSpec
ds.ApplyPackSpecsFunc = func(ctx context.Context, specs []*fleet.PackSpec) error {
appliedPacks = specs
return nil
}
name := writeTmpYml(t, packsSpec)
assert.Equal(t, "[+] applied 1 pack\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyPackSpecsFuncInvoked)
require.Len(t, appliedPacks, 1)
assert.Equal(t, "osquery_monitoring", appliedPacks[0].Name)
require.Len(t, appliedPacks[0].Queries, 2)
interval := writeTmpYml(t, `---
apiVersion: v1
kind: pack
spec:
name: test_bad_interval
reports:
- query: good_interval
name: good_interval
interval: 7200
- query: bad_interval
name: bad_interval
interval: 604801
`)
expectedErrMsg := "applying packs: POST /api/latest/fleet/spec/packs received status 400 Bad request: pack payload verification: pack scheduled query interval must be an integer greater than 1 and less than 604800"
_, err := runAppNoChecks([]string{"apply", "-f", interval})
assert.Error(t, err)
require.Contains(t, err.Error(), expectedErrMsg)
}
func TestApplyQueries(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
var appliedQueries []*fleet.Query
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, &notFoundError{}
}
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{}) error {
appliedQueries = queries
return nil
}
name := writeTmpYml(t, queriesSpec)
assert.Equal(t, "[+] applied 1 report\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.ApplyQueriesFuncInvoked)
require.Len(t, appliedQueries, 1)
assert.Equal(t, "app_schemes", appliedQueries[0].Name)
assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query)
}
func TestCanApplyIntervalsInNanoseconds(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
// Stubs
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
}
var savedAppConfig *fleet.AppConfig
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
webhook_settings:
interval: 30000000000
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.Equal(t, savedAppConfig.WebhookSettings.Interval.Duration, 30*time.Second)
}
func TestCanApplyIntervalsUsingDurations(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t)
// Stubs
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
}
var savedAppConfig *fleet.AppConfig
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
return nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
spec:
webhook_settings:
interval: 30s
`)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
require.Equal(t, savedAppConfig.WebhookSettings.Interval.Duration, 30*time.Second)
}
func TestApplyMacosSetup(t *testing.T) {
mockStore := struct {
sync.Mutex
appConfig *fleet.AppConfig
metaHash []byte
}{}
setupServer := func(t *testing.T, premium bool) *mock.Store {
tier := fleet.TierFree
if premium {
tier = fleet.TierPremium
}
license := &fleet.LicenseInfo{Tier: tier, Expiration: time.Now().Add(24 * time.Hour)}
depStorage := SetupMockDEPStorageAndMockDEPServer(t)
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license, DEPStorage: depStorage})
mockEmptyTeamSoftware(ds)
tm1 := &fleet.Team{ID: 1, Name: "tm1", Config: fleet.TeamConfig{
Features: fleet.Features{
HistoricalData: fleet.HistoricalDataSettings{
Uptime: true,
Vulnerabilities: true,
},
},
}}
teamsByName := map[string]*fleet.Team{
"tm1": tm1,
}
teamsByID := map[uint]*fleet.Team{
tm1.ID: tm1,
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
team, ok := teamsByName[name]
if !ok {
return nil, &notFoundError{}
}
clone := *team
return &clone, nil
}
tmID := 1 // new teams will start at 2
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
tmID++
team.ID = uint(tmID) //nolint:gosec // dismiss G115
clone := *team
teamsByName[team.Name] = &clone
teamsByID[team.ID] = &clone
return team, nil
}
ds.TeamWithExtrasFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
tm, ok := teamsByID[id]
if !ok {
return nil, &notFoundError{}
}
clone := *tm
return &clone, nil
}
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
tms := make([]*fleet.Team, 0, len(teamsByName))
for _, tm := range teamsByName {
clone := *tm
tms = append(tms, &clone)
}
sort.Slice(tms, func(i, j int) bool {
l, r := tms[i], tms[j]
return l.Name < r.Name
})
return tms, nil
}
// initialize mockConfig
mockStore.Lock()
mockStore.appConfig = &fleet.AppConfig{
OrgInfo: fleet.OrgInfo{OrgName: "Fleet"},
ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
MDM: fleet.MDM{EnabledAndConfigured: true},
SMTPSettings: &fleet.SMTPSettings{},
SSOSettings: &fleet.SSOSettings{},
Features: fleet.Features{
HistoricalData: fleet.HistoricalDataSettings{
Uptime: true,
Vulnerabilities: true,
},
},
}
if premium {
mockStore.appConfig.ServerSettings.EnableAnalytics = true
}
mockStore.Unlock()
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
mockStore.Lock()
defer mockStore.Unlock()
clone, err := mockStore.appConfig.Clone()
return clone.(*fleet.AppConfig), err
}
ds.SaveAppConfigFunc = func(ctx context.Context, info *fleet.AppConfig) error {
mockStore.Lock()
defer mockStore.Unlock()
clone, err := info.Clone()
if err != nil {
return err
}
mockStore.appConfig = clone.(*fleet.AppConfig)
return nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
return true, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
teamsByName[team.Name] = team
teamsByID[team.ID] = team
return team, nil
}
asstsByTeam := make(map[uint]*fleet.MDMAppleSetupAssistant)
asstID := 0
ds.SetOrUpdateMDMAppleSetupAssistantFunc = func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) {
asstID++
asst.ID = uint(asstID) //nolint:gosec // dismiss G115
asst.UploadedAt = time.Now()
var tmID uint
if asst.TeamID != nil {
tmID = *asst.TeamID
}
asstsByTeam[tmID] = asst
return asst, nil
}
ds.DeleteMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) error {
var tmID uint
if teamID != nil {
tmID = *teamID
}
delete(asstsByTeam, tmID)
return nil
}
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
var tmID uint
if teamID != nil {
tmID = *teamID
}
if asst, ok := asstsByTeam[tmID]; ok {
return asst, nil
}
return nil, &notFoundError{}
}
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
return nil
}
ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
return nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, nil
}
ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
}
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
return 0, nil
}
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.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.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return nil, &notFoundError{}
}
return ds
}
emptyMacosSetup := writeTmpJSON(t, map[string]any{})
invalidURLMacosSetup := writeTmpJSON(t, map[string]any{
"url": "https://example.com",
})
invalidAwaitMacosSetup := writeTmpJSON(t, map[string]any{
"await_device_configured": true,
})
const (
appConfigSpec = `
apiVersion: v1
kind: config
spec:
mdm:
setup_experience:
macos_bootstrap_package: %s
apple_setup_assistant: %s
`
appConfigEnableReleaseSpec = appConfigSpec + `
apple_enable_release_device_manually: %s
`
appConfigNoKeySpec = `
apiVersion: v1
kind: config
spec:
mdm:
setup_experience:
`
appConfigSpecEnableEndUserAuth = `
apiVersion: v1
kind: config
spec:
mdm:
setup_experience:
enable_end_user_authentication: %s
`
team1Spec = `
apiVersion: v1
kind: fleet
spec:
team:
name: tm1
mdm:
setup_experience:
macos_bootstrap_package: %s
apple_setup_assistant: %s
`
team1EnableReleaseSpec = team1Spec + `
apple_enable_release_device_manually: %s
require_all_software_macos: %s
`
team1NoKeySpec = `
apiVersion: v1
kind: fleet
spec:
team:
name: tm1
mdm:
setup_experience:
`
team1And2Spec = `
apiVersion: v1
kind: fleet
spec:
team:
name: tm1
mdm:
setup_experience:
macos_bootstrap_package: %s
apple_setup_assistant: %s
---
apiVersion: v1
kind: fleet
spec:
team:
name: tm2
mdm:
setup_experience:
macos_bootstrap_package: %s
apple_setup_assistant: %s
`
team1SpecEnableEndUserAuth = `
apiVersion: v1
kind: fleet
spec:
team:
name: tm1
mdm:
setup_experience:
enable_end_user_authentication: %s
`
)
t.Run("free license", func(t *testing.T) {
ds := setupServer(t, false)
// appconfig macos setup assistant
name := writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", emptyMacosSetup))
runAppCheckErr(t, []string{"apply", "-f", name}, `uploading apple setup assistant: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "https://example.com", ""))
runAppCheckErr(t, []string{"apply", "-f", name}, `verifying bootstrap package: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
// team macos setup assistant
name = writeTmpYml(t, fmt.Sprintf(team1Spec, "", emptyMacosSetup))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleets: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
name = writeTmpYml(t, fmt.Sprintf(team1Spec, "https://example.com", ""))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleets: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
// enable_end_user_authentication is premium only
name = writeTmpYml(t, fmt.Sprintf(appConfigSpecEnableEndUserAuth, "true"))
runAppCheckErr(t, []string{"apply", "-f", name},
`applying fleet config: PATCH /api/latest/fleet/config received status 422 Validation Failed: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
name = writeTmpYml(t, fmt.Sprintf(team1SpecEnableEndUserAuth, "true"))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleets: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
})
t.Run("setup assistant invalid file, not json, invalid json", func(t *testing.T) {
ds := setupServer(t, true)
// create invalid json file
tmpFile, err := os.CreateTemp(t.TempDir(), "*.json")
require.NoError(t, err)
_, err = tmpFile.WriteString(`not json`)
require.NoError(t, err)
invalidJSON := tmpFile.Name()
// appconfig invalid file
name := writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", "no_such_file.json"))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `no such file`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
// appconfig not .json
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", "no_such_file.txt"))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `Couldnt edit apple_setup_assistant. The file should be a .json file.`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
// appconfig invalid json
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", invalidJSON))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `The file should include valid JSON`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
// team invalid file
name = writeTmpYml(t, fmt.Sprintf(team1Spec, "", "no_such_file.json"))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `no such file`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
// team not .json
name = writeTmpYml(t, fmt.Sprintf(team1Spec, "", "no_such_file.txt"))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `Couldnt edit apple_setup_assistant. The file should be a .json file.`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
// team invalid json
name = writeTmpYml(t, fmt.Sprintf(team1Spec, "", invalidJSON))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `The file should include valid JSON`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
})
t.Run("setup assistant get and apply roundtrip", func(t *testing.T) {
ds := setupServer(t, true)
b, err := os.ReadFile(filepath.Join("testdata", "macosSetupExpectedAppConfigEmpty.yml"))
require.NoError(t, err)
expectedEmptyAppCfg := string(b)
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedAppConfigSet.yml"))
require.NoError(t, err)
expectedAppCfgSet := fmt.Sprintf(string(b), "", emptyMacosSetup, "", emptyMacosSetup)
expectedAppCfgSetReleaseEnabled := strings.ReplaceAll(expectedAppCfgSet, `enable_release_device_manually: false`, `enable_release_device_manually: true`)
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Empty.yml"))
require.NoError(t, err)
expectedEmptyTm1 := string(b)
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Set.yml"))
require.NoError(t, err)
expectedTm1Set := fmt.Sprintf(string(b), "", "", "", "")
expectedTm1SetReleaseAndRequireEnabled := strings.ReplaceAll(expectedTm1Set, `enable_release_device_manually: false`, `enable_release_device_manually: true`)
expectedTm1SetReleaseAndRequireEnabled = strings.ReplaceAll(expectedTm1SetReleaseAndRequireEnabled, `require_all_software_macos: false`, `require_all_software_macos: true`)
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1And2Empty.yml"))
require.NoError(t, err)
expectedEmptyTm1And2 := string(b)
b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1And2Set.yml"))
require.NoError(t, err)
expectedTm1And2Set := fmt.Sprintf(string(b), "", emptyMacosSetup, "", emptyMacosSetup, "", emptyMacosSetup, "", emptyMacosSetup)
// get without setup assistant set
assert.YAMLEq(t, expectedEmptyAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
assert.YAMLEq(t, expectedEmptyTm1, runAppForTest(t, []string{"get", "teams", "--yaml"}))
// apply with dry-run, appconfig
name := writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", emptyMacosSetup))
assert.Equal(t, "[+] would've applied fleet config\n", runAppForTest(t, []string{"apply", "--dry-run", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
// apply with dry-run, teams
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", emptyMacosSetup, "", emptyMacosSetup))
assert.Equal(t, "[+] would've applied 2 fleets\n", runAppForTest(t, []string{"apply", "--dry-run", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
// get, setup assistant still not set
assert.YAMLEq(t, expectedEmptyAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
assert.YAMLEq(t, expectedEmptyTm1, runAppForTest(t, []string{"get", "fleets", "--yaml"}))
// apply appconfig for real, and enable release device
name = writeTmpYml(t, fmt.Sprintf(appConfigEnableReleaseSpec, "", emptyMacosSetup, "true"))
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
// apply teams for real
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", emptyMacosSetup, "", emptyMacosSetup))
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
assert.Equal(t, "[+] applied 2 fleets\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveTeamFuncInvoked)
// get, setup assistant is now set
assert.YAMLEq(t, expectedAppCfgSetReleaseEnabled, runAppForTest(t, []string{"get", "config", "--yaml"}))
assert.YAMLEq(t, expectedTm1And2Set, runAppForTest(t, []string{"get", "fleets", "--yaml"}))
// clear with dry-run, appconfig
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", ""))
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
ds.SaveAppConfigFuncInvoked = false
assert.Equal(t, "[+] would've applied fleet config\n", runAppForTest(t, []string{"apply", "--dry-run", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
// clear with dry-run, teams
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", "", "", ""))
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
ds.SaveTeamFuncInvoked = false
assert.Equal(t, "[+] would've applied 2 fleets\n", runAppForTest(t, []string{"apply", "--dry-run", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.SaveTeamFuncInvoked)
// apply appconfig without the setup assistant key
name = writeTmpYml(t, appConfigNoKeySpec)
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
// apply team 1 without the setup assistant key
name = writeTmpYml(t, team1NoKeySpec)
assert.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveTeamFuncInvoked)
// get, results unchanged
assert.YAMLEq(t, expectedAppCfgSetReleaseEnabled, runAppForTest(t, []string{"get", "config", "--yaml"}))
assert.YAMLEq(t, expectedTm1And2Set, runAppForTest(t, []string{"get", "fleets", "--yaml"}))
// clear appconfig for real
name = writeTmpYml(t, fmt.Sprintf(appConfigEnableReleaseSpec, "", "", "false"))
ds.SaveAppConfigFuncInvoked = false
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
// clear teams for real
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", "", "", ""))
ds.SaveTeamFuncInvoked = false
assert.Equal(t, "[+] applied 2 fleets\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveTeamFuncInvoked)
// get, results now empty
assert.YAMLEq(t, expectedEmptyAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
assert.YAMLEq(t, expectedEmptyTm1And2, runAppForTest(t, []string{"get", "fleets", "--yaml"}))
// apply team 1 without the setup assistant key but enable device release
name = writeTmpYml(t, fmt.Sprintf(team1EnableReleaseSpec, "", "", "true", "true"))
ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false
ds.DeleteMDMAppleSetupAssistantFuncInvoked = false
ds.SaveTeamFuncInvoked = false
assert.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.DeleteMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.SaveTeamFuncInvoked)
assert.YAMLEq(t, expectedTm1SetReleaseAndRequireEnabled, runAppForTest(t, []string{"get", "fleets", "--yaml"}))
// apply appconfig with invalid URL key
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", invalidURLMacosSetup))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, "The automatic enrollment profile can't include url.")
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// apply teams with invalid URL key
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", invalidURLMacosSetup, "", invalidURLMacosSetup))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, "The automatic enrollment profile can't include url.")
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// apply appconfig with invalid await_device_configured key
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", invalidAwaitMacosSetup))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `The profile can't include "await_device_configured" option.`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// apply teams with invalid await_device_configured key
name = writeTmpYml(t, fmt.Sprintf(team1And2Spec, "", invalidAwaitMacosSetup, "", invalidAwaitMacosSetup))
_, err = runAppNoChecks([]string{"apply", "-f", name})
require.ErrorContains(t, err, `The profile can't include "await_device_configured" option.`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
})
t.Run("require_all_software_windows", func(t *testing.T) {
ds := setupServer(t, true)
// Enable Windows MDM in the app config.
mockStore.Lock()
mockStore.appConfig.MDM.WindowsEnabledAndConfigured = true
mockStore.Unlock()
b, err := os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Set.yml"))
require.NoError(t, err)
expectedTm1 := fmt.Sprintf(string(b), "", "", "", "")
// Apply team with require_all_software_windows enabled.
windowsRequireSpec := `
apiVersion: v1
kind: fleet
spec:
team:
name: tm1
mdm:
setup_experience:
require_all_software_windows: true
`
name := writeTmpYml(t, windowsRequireSpec)
assert.Equal(t, "[+] applied 1 fleet\n", runAppForTest(t, []string{"apply", "-f", name}))
assert.True(t, ds.SaveTeamFuncInvoked)
// Verify the output includes require_all_software_windows: true.
expectedWithWindowsRequire := strings.ReplaceAll(expectedTm1, `require_all_software_windows: false`, `require_all_software_windows: true`)
assert.YAMLEq(t, expectedWithWindowsRequire, runAppForTest(t, []string{"get", "teams", "--yaml"}))
})
t.Run("require_all_software_windows rejected when Windows MDM not configured", func(t *testing.T) {
// Spec invariant: setting `require_all_software_windows=true` while
// `MDM.WindowsEnabledAndConfigured=false` MUST be rejected. setupServer's default appConfig leaves
// WindowsEnabledAndConfigured at the zero value (false), which is the precondition this test needs.
ds := setupServer(t, true)
windowsRequireSpec := `
apiVersion: v1
kind: fleet
spec:
team:
name: tm1
mdm:
setup_experience:
require_all_software_windows: true
`
name := writeTmpYml(t, windowsRequireSpec)
runAppCheckErr(t, []string{"apply", "-f", name}, "require_all_software_windows")
assert.False(t, ds.SaveTeamFuncInvoked,
"team must not be saved when require_all_software_windows is rejected")
})
t.Run("new bootstrap package", func(t *testing.T) {
cases := []struct {
pkgName string
expectedErr error
}{
{"signed.pkg", nil},
{"unsigned.pkg", errors.New("verifying bootstrap package: Couldnt edit macos_bootstrap_package. The macos_bootstrap_package must be signed. Learn how to sign the package in the Fleet documentation: https://fleetdm.com/learn-more-about/setup-experience/bootstrap-package")},
{"invalid.tar.gz", errors.New("verifying bootstrap package: Couldnt edit macos_bootstrap_package. The file must be a package (.pkg).")},
{"wrong-toc.pkg", errors.New("verifying bootstrap package: checking package signature: decompressing TOC: unexpected EOF")},
}
for _, c := range cases {
t.Run(c.pkgName, func(t *testing.T) {
srv, pkgLen := testing_utils.ServeMDMBootstrapPackage(t,
filepath.Join("../../../server/service/testdata/bootstrap-packages", c.pkgName), c.pkgName)
ds := setupServer(t, true)
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
require.Equal(t, len(bp.Bytes), pkgLen)
return nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, &notFoundError{}
}
mockStore.Lock()
assert.Equal(t, "", mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
// create the app config yaml with server url for bootstrap package
tmpFilename := writeTmpYml(t, fmt.Sprintf(appConfigSpec, srv.URL, ""))
if c.expectedErr != nil {
runAppCheckErr(t, []string{"apply", "-f", tmpFilename}, c.expectedErr.Error())
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.SaveAppConfigFuncInvoked)
mockStore.Lock()
assert.Equal(t, "", mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
} else {
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", tmpFilename}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.True(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
mockStore.Lock()
assert.Equal(t, srv.URL, mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
}
})
}
})
t.Run("bootstrap package with manual agent install", func(t *testing.T) {
pkgName := "signed.pkg"
srv, pkgLen := testing_utils.ServeMDMBootstrapPackage(t, filepath.Join("../../../server/service/testdata/bootstrap-packages", pkgName),
pkgName)
ds := setupServer(t, true)
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
require.Equal(t, len(bp.Bytes), pkgLen)
return nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, &notFoundError{}
}
mockStore.Lock()
assert.Equal(t, "", mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
spec := `
apiVersion: v1
kind: config
spec:
mdm:
setup_experience:
macos_bootstrap_package: %s
macos_manual_agent_install: true
`
// create the app config yaml with server url for bootstrap package
tmpFilename := writeTmpYml(t, fmt.Sprintf(spec, srv.URL))
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", tmpFilename}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.True(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
mockStore.Lock()
assert.Equal(t, srv.URL, mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
assert.True(t, mockStore.appConfig.MDM.MacOSSetup.ManualAgentInstall.Value)
mockStore.Unlock()
})
t.Run("replace bootstrap package", func(t *testing.T) {
pkgName := "signed.pkg"
pkgBytes, err := os.ReadFile(filepath.Join("../../../server/service/testdata/bootstrap-packages", pkgName))
require.NoError(t, err)
pkgHash := sha256.New()
n, err := io.Copy(pkgHash, bytes.NewReader(pkgBytes))
require.NoError(t, err)
require.Equal(t, int64(len(pkgBytes)), n)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(pkgBytes)))
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, pkgName))
if n, err := w.Write(pkgBytes); err != nil {
require.NoError(t, err)
require.Equal(t, len(pkgBytes), n)
}
}))
defer srv.Close()
ds := setupServer(t, true)
ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
mockStore.Lock()
defer mockStore.Unlock()
require.Equal(t, pkgName, bp.Name)
require.Equal(t, len(bp.Bytes), len(pkgBytes))
require.Equal(t, pkgHash.Sum(nil), bp.Sha256)
mockStore.metaHash = bp.Sha256
return nil
}
ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
require.Equal(t, uint(0), teamID)
return nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
mockStore.Lock()
defer mockStore.Unlock()
return &fleet.MDMAppleBootstrapPackage{
TeamID: 0,
Name: pkgName,
Sha256: mockStore.metaHash,
Token: "token",
CreatedAt: time.Now().Add(-1 * time.Hour),
UpdatedAt: time.Now().Add(-1 * time.Hour),
}, nil
}
mockStore.Lock()
mockStore.metaHash = []byte("foobar") // initial hash is a throwaway
mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage = optjson.SetString("https://example.com") // initial value is a throwaway
mockStore.Unlock()
// upload a new package
tmpFilename := writeTmpYml(t, fmt.Sprintf(appConfigSpec, srv.URL, ""))
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", tmpFilename}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.True(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
mockStore.Lock()
assert.Equal(t, srv.URL, mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
ds.GetMDMAppleBootstrapPackageMetaFuncInvoked = false
ds.InsertMDMAppleBootstrapPackageFuncInvoked = false
ds.DeleteMDMAppleBootstrapPackageFuncInvoked = false
ds.SaveAppConfigFuncInvoked = false
// running again should not re-upload
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", tmpFilename}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
mockStore.Lock()
assert.Equal(t, srv.URL, mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
ds.GetMDMAppleBootstrapPackageMetaFuncInvoked = false
ds.InsertMDMAppleBootstrapPackageFuncInvoked = false
ds.DeleteMDMAppleBootstrapPackageFuncInvoked = false
ds.SaveAppConfigFuncInvoked = false
// empty server url should delete the package
tmpFilename = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", ""))
assert.Equal(t, "[+] applied fleet config\n", runAppForTest(t, []string{"apply", "-f", tmpFilename}))
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.True(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
assert.True(t, ds.SaveAppConfigFuncInvoked)
mockStore.Lock()
assert.Equal(t, "", mockStore.appConfig.MDM.MacOSSetup.BootstrapPackage.Value)
mockStore.Unlock()
})
// // TODO: restore this test when we have a way to mock the Apple Business API in
// // fleetctl tests
// t.Run("enable end user authentication", func(t *testing.T) {
// ds := setupServer(t, true)
// // setup app config
// b, err := os.ReadFile(filepath.Join("testdata", "macosSetupExpectedAppConfigEmpty.yml"))
// require.NoError(t, err)
// expectedNotSetAppConfg := string(b)
// assert.YAMLEq(t, expectedNotSetAppConfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
// // enable end user auth in app config
// name := writeTmpYml(t, fmt.Sprintf(appConfigSpecEnableEndUserAuth, "true"))
// _, err = runAppNoChecks([]string{"apply", "-f", name})
// require.NoError(t, err)
// assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
// assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
// assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
// assert.True(t, ds.SaveAppConfigFuncInvoked)
// expectedSetAppCfg := strings.ReplaceAll(expectedNotSetAppConfg, "enable_end_user_authentication: false", "enable_end_user_authentication: true")
// assert.YAMLEq(t, expectedSetAppCfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
// ds.SaveAppConfigFuncInvoked = false
// // disable end user auth in app config
// name = writeTmpYml(t, fmt.Sprintf(appConfigSpecEnableEndUserAuth, "false"))
// _, err = runAppNoChecks([]string{"apply", "-f", name})
// require.NoError(t, err)
// assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
// assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
// assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
// assert.True(t, ds.SaveAppConfigFuncInvoked)
// assert.YAMLEq(t, expectedNotSetAppConfg, runAppForTest(t, []string{"get", "config", "--yaml"}))
// ds.SaveAppConfigFuncInvoked = false
// // setup team config
// assert.False(t, ds.SaveTeamFuncInvoked)
// b, err = os.ReadFile(filepath.Join("testdata", "macosSetupExpectedTeam1Empty.yml"))
// require.NoError(t, err)
// expectedNotSetTeam1 := string(b)
// assert.YAMLEq(t, expectedNotSetTeam1, runAppForTest(t, []string{"get", "teams", "--yaml"}))
// // enable end user auth in team config
// name = writeTmpYml(t, fmt.Sprintf(team1SpecEnableEndUserAuth, "true"))
// _, err = runAppNoChecks([]string{"apply", "-f", name})
// require.NoError(t, err)
// assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
// assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
// assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
// assert.False(t, ds.SaveAppConfigFuncInvoked)
// assert.True(t, ds.SaveTeamFuncInvoked)
// expectedSetTeam1 := strings.ReplaceAll(expectedNotSetTeam1, "enable_end_user_authentication: false", "enable_end_user_authentication: true")
// expectedSetTeam1 = strings.ReplaceAll(expectedSetTeam1, "enable_host_users: false", "enable_host_users: true")
// expectedSetTeam1 = strings.ReplaceAll(expectedSetTeam1, "enable_software_inventory: false", "enable_software_inventory: true")
// assert.YAMLEq(t, expectedSetTeam1, runAppForTest(t, []string{"get", "teams", "--yaml"}))
// ds.SaveTeamFuncInvoked = false
// // disable end user auth in team config
// name = writeTmpYml(t, fmt.Sprintf(team1SpecEnableEndUserAuth, "false"))
// _, err = runAppNoChecks([]string{"apply", "-f", name})
// require.NoError(t, err)
// assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
// assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
// assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
// assert.False(t, ds.DeleteMDMAppleBootstrapPackageFuncInvoked)
// assert.False(t, ds.SaveAppConfigFuncInvoked)
// assert.True(t, ds.SaveTeamFuncInvoked)
// expectedSetTeam1 = strings.ReplaceAll(expectedSetTeam1, "enable_end_user_authentication: true", "enable_end_user_authentication: false")
// assert.YAMLEq(t, expectedSetTeam1, runAppForTest(t, []string{"get", "teams", "--yaml"}))
// ds.SaveTeamFuncInvoked = false
// })
}
func TestApplySpecs(t *testing.T) {
// create a macos setup json file (content not important)
macSetupFile := writeTmpJSON(t, map[string]any{})
setupDS := func(ds *mock.Store) {
// labels
ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorId *uint) error {
return nil
}
ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
}
// teams - team ID 1 already exists
teamsByName := map[string]*fleet.Team{
"team1": {
ID: 1,
Name: "team1",
Description: "team1 description",
},
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
team, ok := teamsByName[name]
if !ok {
return nil, &notFoundError{}
}
return team, nil
}
i := 1 // new teams will start at 2
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
i++
team.ID = uint(i) //nolint:gosec // dismiss G115
teamsByName[team.Name] = team
return team, nil
}
agentOpts := json.RawMessage(`{"config":{}}`)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{AgentOptions: &agentOpts}, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
teamsByName[team.Name] = team
return team, nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
return true, nil
}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
return nil
}
// app config
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return userRoleSpecList, nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "admin1@example.com" {
return userRoleSpecList[0], nil
}
return userRoleSpecList[1], nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{OrgInfo: fleet.OrgInfo{OrgName: "Fleet"}, ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"}}, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
return nil
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
return nil
}
// VPP/AMB
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.HasAppleUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
ds.HasWindowsUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
}
cases := []struct {
desc string
flags []string
spec string
wantOutput string
wantErr string
}{
{
desc: "empty team spec",
spec: `
apiVersion: v1
kind: fleet
spec:
`,
wantOutput: "[+] applied 1 fleet",
},
{
desc: "empty team name",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: ""
`,
wantErr: `422 Validation Failed: name may not be empty`,
},
{
desc: "invalid agent options for existing team",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
agent_options:
config:
blah: nope
`,
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
},
{
desc: "invalid top-level key for team",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
blah: nope
`,
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
},
{
desc: "invalid known key's value type for team cannot be forced",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: 123
`,
flags: []string{"--force"},
wantErr: `400 Bad Request: invalid value type at 'specs.name': expected string but got number`,
},
{
desc: "unknown key for team can be forced",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
blah: true
`,
flags: []string{"--force"},
wantOutput: `[+] applied 1 fleet`,
},
{
desc: "invalid agent options for new team",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
config:
blah: nope
`,
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
},
{
desc: "invalid agent options dry-run",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
config:
blah: nope
`,
flags: []string{"--dry-run"},
wantErr: `400 Bad Request: unsupported key provided: "blah"`,
},
{
desc: "invalid agent options force",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
config:
blah: nope
`,
flags: []string{"--force"},
wantOutput: `[+] applied 1 fleet`,
},
{
desc: "invalid agent options field type",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
config:
options:
aws_debug: 123
`,
flags: []string{"--dry-run"},
wantErr: `400 Bad Request: invalid value type at 'options.aws_debug': expected bool but got number`,
},
{
desc: "invalid team agent options command-line flag",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
command_line_flags:
no_such_flag: 123
`,
wantErr: `400 Bad Request: unsupported key provided: "no_such_flag"`,
},
{
desc: "valid team agent options command-line flag",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
command_line_flags:
enable_tables: "abc"
`,
wantOutput: `[+] applied 1 fleet`,
},
{
desc: "invalid agent options field type in overrides",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
agent_options:
config:
options:
aws_debug: true
overrides:
platforms:
darwin:
options:
aws_debug: 123
`,
wantErr: `400 Bad Request: invalid value type at 'options.aws_debug': expected bool but got number`,
},
{
desc: "empty config",
spec: `
apiVersion: v1
kind: config
spec:
`,
wantOutput: ``, // no output for empty config
},
{
desc: "config with blank required org name",
spec: `
apiVersion: v1
kind: config
spec:
org_info:
org_name: ""
`,
wantErr: `422 Validation Failed: organization name must be present`,
},
{
desc: "config with blank required server url",
spec: `
apiVersion: v1
kind: config
spec:
server_settings:
server_url: ""
`,
wantErr: `422 Validation Failed: Fleet server URL must be present`,
},
{
desc: "config with unknown key",
spec: `
apiVersion: v1
kind: config
spec:
server_settings:
foo: bar
`,
wantErr: `400 Bad Request: unsupported key provided: "foo"`,
},
{
desc: "config with invalid key type",
spec: `
apiVersion: v1
kind: config
spec:
server_settings:
server_url: 123
`,
wantErr: `400 Bad request: failed to decode app config`,
},
{
desc: "config with invalid agent options in dry-run",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
foo: bar
`,
flags: []string{"--dry-run"},
wantErr: `400 Bad Request: unsupported key provided: "foo"`,
},
{
desc: "config with invalid agent options data type in dry-run",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
config:
options:
aws_debug: 123
`,
flags: []string{"--dry-run"},
wantErr: `400 Bad Request: invalid value type at 'options.aws_debug': expected bool but got number`,
},
{
desc: "config with invalid agent options data type with force",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
config:
options:
aws_debug: 123
`,
flags: []string{"--force"},
wantOutput: `[+] applied fleet config`,
},
{
desc: "config with invalid agent options command-line flags",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
command_line_flags:
enable_tables: "foo"
no_such_flag: false
`,
wantErr: `400 Bad Request: unsupported key provided: "no_such_flag"`,
},
{
desc: "config with invalid value for agent options command-line flags",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
command_line_flags:
enable_tables: 123
`,
wantErr: `400 Bad Request: invalid value type at 'enable_tables': expected string but got number`,
},
{
desc: "config with valid agent options command-line flags",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
command_line_flags:
enable_tables: "abc"
`,
wantOutput: `[+] applied fleet config`,
},
{
desc: "dry-run set with unsupported spec",
spec: `
apiVersion: v1
kind: label
spec:
name: label1
query: SELECT 1
`,
flags: []string{"--dry-run"},
wantOutput: `[!] ignoring labels, dry run mode only supported for 'config' and 'fleet' specs`,
},
{
desc: "dry-run set with various specs, appconfig warning for legacy",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
---
apiVersion: v1
kind: label
spec:
name: label1
query: SELECT 1
---
apiVersion: v1
kind: config
spec:
host_settings:
enable_software_inventory: true
`,
flags: []string{"--dry-run"},
wantErr: `400 Bad request: warning: deprecated settings were used in the configuration: [host_settings]`,
wantOutput: `[!] ignoring labels, dry run mode only supported for 'config' and 'fleet' spec`,
},
{
desc: "dry-run set with various specs, no errors",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: teamNEW
---
apiVersion: v1
kind: label
spec:
name: label1
query: SELECT 1
---
apiVersion: v1
kind: config
spec:
features:
enable_software_inventory: true
`,
flags: []string{"--dry-run"},
wantOutput: `[!] ignoring labels, dry run mode only supported for 'config' and 'fleet' specs
[+] would've applied fleet config
[+] would've applied 1 fleet`,
},
{
desc: "macos_updates deadline set but minimum_version empty",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
macos_updates:
deadline: 2022-01-04
`,
wantErr: `422 Validation Failed: minimum_version is required when deadline is provided`,
},
{
desc: "macos_updates minimum_version set but deadline empty",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
macos_updates:
minimum_version: "12.2"
`,
wantErr: `422 Validation Failed: deadline is required when minimum_version is provided`,
},
{
desc: "macos_updates.minimum_version with build version",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
macos_updates:
minimum_version: "12.2 (ABCD)"
deadline: 1892-01-01
`,
wantErr: `422 Validation Failed: minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`,
},
{
desc: "macos_updates.deadline with timestamp",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
macos_updates:
minimum_version: "12.2"
deadline: "1892-01-01T00:00:00Z"
`,
wantErr: fmt.Sprintf(`422 Validation Failed: %s`, fleet.AppleOSVersionDeadlineInvalidMessage),
},
{
desc: "macos_updates.deadline with invalid date",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
macos_updates:
minimum_version: "12.2"
deadline: "18-01-01"
`,
wantErr: fmt.Sprintf(`422 Validation Failed: %s`, fleet.AppleOSVersionDeadlineInvalidMessage),
},
{
desc: "macos_updates.deadline with incomplete date",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
macos_updates:
minimum_version: "12.2"
deadline: "2022-01"
`,
wantErr: fmt.Sprintf(`422 Validation Failed: %s`, fleet.AppleOSVersionDeadlineInvalidMessage),
},
{
desc: "windows_updates.deadline_days but grace period empty",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days: 5
`,
wantErr: `422 Validation Failed: grace_period_days is required when deadline_days is provided`,
},
{
desc: "windows_updates.grace_period_days but deadline empty",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
grace_period_days: 5
`,
wantErr: `422 Validation Failed: deadline_days is required when grace_period_days is provided`,
},
{
desc: "windows_updates.deadline_days out of range",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days: 9999
grace_period_days: 1
`,
wantErr: `422 Validation Failed: deadline_days must be an integer between 0 and 30`,
},
{
desc: "windows_updates.grace_period_days out of range",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days: 1
grace_period_days: 9999
`,
wantErr: `422 Validation Failed: grace_period_days must be an integer between 0 and 7`,
},
{
desc: "windows_updates.deadline_days not a number",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days: abc
grace_period_days: 1
`,
wantErr: `400 Bad Request: invalid value type at 'specs.mdm.windows_updates.deadline_days': expected int but got string`,
},
{
desc: "windows_updates.grace_period_days not a number",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days: 1
grace_period_days: true
`,
wantErr: `400 Bad Request: invalid value type at 'specs.mdm.windows_updates.grace_period_days': expected int but got bool`,
},
{
desc: "windows_updates valid",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days: 5
grace_period_days: 1
`,
wantOutput: `[+] applied 1 fleet`,
},
{
desc: "windows_updates unset valid",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
windows_updates:
deadline_days:
grace_period_days:
`,
wantOutput: `[+] applied 1 fleet`,
},
{
desc: "missing required sso entity_id",
spec: fmt.Sprintf(`
apiVersion: v1
kind: config
spec:
sso_settings:
enable_sso: true
entity_id: ""
issuer_uri: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php"
idp_name: "SimpleSAML"
metadata_url: "%s"
`, testSAMLIDPMetadataURL),
wantErr: `422 Validation Failed: required`,
},
{
desc: "missing required sso idp_name",
spec: fmt.Sprintf(`
apiVersion: v1
kind: config
spec:
sso_settings:
enable_sso: true
entity_id: "https://localhost:8080"
issuer_uri: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php"
idp_name: ""
metadata_url: "%s"
`, testSAMLIDPMetadataURL),
wantErr: `422 Validation Failed: required`,
},
{
desc: "missing required failing policies destination_url",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
failing_policies_webhook:
enable_failing_policies_webhook: true
destination_url: ""
policy_ids:
- 1
host_batch_size: 1000
interval: 1h
`,
wantErr: `422 Validation Failed: destination_url is required to enable the failing policies webhook`,
},
{
desc: "missing required vulnerabilities destination_url",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
vulnerabilities_webhook:
enable_vulnerabilities_webhook: true
destination_url: ""
host_batch_size: 1000
interval: 1h
`,
wantErr: `422 Validation Failed: destination_url is required to enable the vulnerabilities webhook`,
},
{
desc: "missing required host status destination_url",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
host_status_webhook:
enable_host_status_webhook: true
destination_url: ""
days_count: 10
host_percentage: 10
interval: 1h
`,
wantErr: `422 Validation Failed: destination_url is required to enable the host status webhook`,
},
{
desc: "missing required host status days_count",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
host_status_webhook:
enable_host_status_webhook: true
destination_url: "http://some/url"
days_count: 0
host_percentage: 10
interval: 1h
`,
wantErr: `422 Validation Failed: days_count must be > 0 to enable the host status webhook`,
},
{
desc: "missing required host status host_percentage",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
host_status_webhook:
enable_host_status_webhook: true
destination_url: "http://some/url"
days_count: 10
host_percentage: -1
interval: 1h
`,
wantErr: `422 Validation Failed: host_percentage must be > 0 to enable the host status webhook`,
},
{
desc: "config with FIM values for agent options (#8699)",
spec: `
apiVersion: v1
kind: config
spec:
agent_options:
config:
file_paths:
ssh:
- /home/%/.ssh/authorized_keys
exclude_paths:
ssh:
- /home/ubuntu/.ssh/authorized_keys
`,
wantOutput: `[+] applied fleet config`,
},
{
desc: "app config macos_updates deadline set but minimum_version empty",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
deadline: 2022-01-04
`,
wantErr: `422 Validation Failed: minimum_version is required when deadline is provided`,
},
{
desc: "app config macos_updates minimum_version set but deadline empty",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: "12.2"
`,
wantErr: `422 Validation Failed: deadline is required when minimum_version is provided`,
},
{
desc: "app config macos_updates.minimum_version with build version",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: "12.2 (ABCD)"
deadline: 1892-01-01
`,
wantErr: `422 Validation Failed: minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`,
},
{
desc: "app config macos_updates.deadline with timestamp",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: "12.2"
deadline: "1892-01-01T00:00:00Z"
`,
wantErr: fmt.Sprintf(`422 Validation Failed: %s`, fleet.AppleOSVersionDeadlineInvalidMessage),
},
{
desc: "app config macos_updates.deadline with invalid date",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: "12.2"
deadline: "18-01-01"
`,
wantErr: fmt.Sprintf(`422 Validation Failed: %s`, fleet.AppleOSVersionDeadlineInvalidMessage),
},
{
desc: "app config macos_updates.deadline with incomplete date",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
macos_updates:
minimum_version: "12.2"
deadline: "2022-01"
`,
wantErr: fmt.Sprintf(`422 Validation Failed: %s`, fleet.AppleOSVersionDeadlineInvalidMessage),
},
{
desc: "app config windows_updates.deadline_days but grace period empty",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days: 5
`,
wantErr: `422 Validation Failed: grace_period_days is required when deadline_days is provided`,
},
{
desc: "app config windows_updates.grace_period_days but deadline empty",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
grace_period_days: 5
`,
wantErr: `422 Validation Failed: deadline_days is required when grace_period_days is provided`,
},
{
desc: "app config windows_updates.deadline_days out of range",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days: 9999
grace_period_days: 1
`,
wantErr: `422 Validation Failed: deadline_days must be an integer between 0 and 30`,
},
{
desc: "app config windows_updates.grace_period_days out of range",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days: 1
grace_period_days: 9999
`,
wantErr: `422 Validation Failed: grace_period_days must be an integer between 0 and 7`,
},
{
desc: "app config windows_updates.deadline_days not a number",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days: abc
grace_period_days: 1
`,
wantErr: `400 Bad request: failed to decode app config`,
},
{
desc: "app config windows_updates.grace_period_days not a number",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days: 1
grace_period_days: true
`,
wantErr: `400 Bad request: failed to decode app config`,
},
{
desc: "app config windows_updates valid",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days: 5
grace_period_days: 1
`,
wantOutput: `[+] applied fleet config`,
},
{
desc: "app config windows_updates unset valid",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_updates:
deadline_days:
grace_period_days:
`,
wantOutput: `[+] applied fleet config`,
},
{
desc: "app config macos_settings.enable_disk_encryption without a value",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
apple_settings:
enable_disk_encryption:
`,
wantOutput: `[+] applied fleet config`,
},
{
desc: "app config macos_settings.enable_disk_encryption with invalid value type",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
apple_settings:
enable_disk_encryption: 123
`,
wantErr: `400 Bad request: failed to decode app config`,
},
{
desc: "app config macos_settings.enable_disk_encryption true",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
apple_settings:
enable_disk_encryption: true
`,
// Since Linux disk encryption does not use MDM, we allow enabling it even without MDM enabled and configured
wantOutput: `[+] applied fleet config`,
},
{
desc: "app config macos_settings.enable_disk_encryption false",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
apple_settings:
enable_disk_encryption: false
`,
wantOutput: `[+] applied fleet config`,
},
{
desc: "team config macos_settings.enable_disk_encryption without a value",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
apple_settings:
enable_disk_encryption:
`,
wantErr: `400 Bad Request: invalid value type at 'macos_settings.enable_disk_encryption': expected bool but got <nil>`,
},
{
desc: "team config macos_settings.enable_disk_encryption with invalid value type",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
apple_settings:
enable_disk_encryption: 123
`,
wantErr: `400 Bad Request: invalid value type at 'macos_settings.enable_disk_encryption': expected bool but got float64`,
},
{
desc: "team config macos_settings.enable_disk_encryption true",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
apple_settings:
enable_disk_encryption: true
`,
wantErr: `Couldn't update apple_settings because MDM features aren't turned on in Fleet.`,
},
{
desc: "team config macos_settings.enable_disk_encryption false",
spec: `
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
apple_settings:
enable_disk_encryption: false
`,
wantOutput: `[+] applied 1 fleet`,
},
{
desc: "team config mac setup assistant",
spec: fmt.Sprintf(`
apiVersion: v1
kind: fleet
spec:
team:
name: team1
mdm:
setup_experience:
apple_setup_assistant: %s
`, macSetupFile),
wantErr: `macOS MDM isn't turned on.`,
},
{
desc: "app config macos setup assistant",
spec: fmt.Sprintf(`
apiVersion: v1
kind: config
spec:
mdm:
setup_experience:
apple_setup_assistant: %s
`, macSetupFile),
wantErr: `macOS MDM isn't turned on.`,
},
{
desc: "app config enable windows mdm without WSTEP",
spec: `
apiVersion: v1
kind: config
spec:
mdm:
windows_enabled_and_configured: true
`,
wantErr: `422 Validation Failed: Couldn't turn on Windows MDM. Please configure Fleet with a certificate and key pair first.`,
},
{
desc: "activities_webhook empty destination_url",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
activities_webhook:
enable_activities_webhook: true
destination_url: ""
`,
wantErr: `422 Validation Failed: destination_url is required`,
},
{
desc: "activities_webhook bad destination_url 1",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
activities_webhook:
enable_activities_webhook: true
destination_url: ftp://host
`,
wantErr: `422 Validation Failed: destination_url must be http`,
},
{
desc: "activities_webhook bad destination_url 2",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
activities_webhook:
enable_activities_webhook: true
destination_url: /foo
`,
wantErr: `422 Validation Failed: destination_url must be http`,
},
{
desc: "activities_webhook bad destination_url 3",
spec: `
apiVersion: v1
kind: config
spec:
webhook_settings:
activities_webhook:
enable_activities_webhook: true
destination_url: foo
`,
wantErr: `422 Validation Failed: parse "foo": invalid URI`,
},
}
// NOTE: Integrations required fields are not tested (Jira/Zendesk) because
// they require a complex setup to mock the client that would communicate
// with the external API. However, we make a test API call when enabling an
// integration, ensuring that any missing configuration field results in an
// error. Same for smtp_settings (a test email is sent when enabling).
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license})
setupDS(ds)
filename := writeTmpYml(t, c.spec)
var got string
if c.wantErr == "" {
got = runAppForTest(t, append([]string{"apply", "-f", filename}, c.flags...))
} else {
buf, err := runAppNoChecks(append([]string{"apply", "-f", filename}, c.flags...))
require.ErrorContains(t, err, c.wantErr)
got = buf.String()
}
if c.wantOutput == "" {
require.Empty(t, got)
} else {
require.Contains(t, got, c.wantOutput)
}
})
}
}
func TestApplyFileExtensionValidation(t *testing.T) {
cases := []struct {
desc string
filename string
wantErr string
}{
{
desc: "Valid .yml extension",
filename: "test_file.yml",
wantErr: "",
},
{
desc: "Valid .yaml extension",
filename: "test_file.yaml",
wantErr: "",
},
{
desc: "Invalid .txt extension",
filename: "test_file.txt",
wantErr: "Invalid file extension .txt: only .yml or .yaml files can be applied",
},
{
desc: "Invalid .json extension",
filename: "test_file.json",
wantErr: "Invalid file extension .json: only .yml or .yaml files can be applied",
},
{
desc: "No extension",
filename: "test_file",
wantErr: "Missing file extension: only .yml or .yaml files can be applied",
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
// Create a temporary directory
tmpDir, err := os.MkdirTemp("", "test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) // clean up
// Create the file with the exact name in the temporary directory
tmpFilePath := filepath.Join(tmpDir, c.filename)
tmpFile, err := os.Create(tmpFilePath)
if err != nil {
t.Fatal(err)
}
tmpFile.Close()
// Create a new cli.App for each test
app := &cli.App{
Commands: []*cli.Command{
applyCommand(),
},
}
// Set up arguments
args := []string{"fleetctl", "apply", "-f", tmpFilePath}
// Run the command
err = app.Run(args)
if c.wantErr == "" {
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
} else {
if err == nil {
t.Errorf("Expected error, but got none")
} else if err.Error() != c.wantErr {
t.Errorf("Expected error message '%s', but got '%s'", c.wantErr, err.Error())
}
}
})
}
}
func TestApplyWindowsUpdates(t *testing.T) {
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(
t, &service.TestServerOpts{
License: license,
KeyValueStore: testing_utils.NewMemKeyValueStore(),
},
)
teamsByName := map[string]*fleet.Team{
"Team1": {
ID: 1,
Name: "Team1",
},
}
// Track calls to Windows updates functions
var setOrUpdateCalls []fleet.MDMWindowsConfigProfile
var deleteCalls []struct {
teamID *uint
name string
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
setOrUpdateCalls = append(setOrUpdateCalls, cp)
return nil
}
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
deleteCalls = append(deleteCalls, struct {
teamID *uint
name string
}{teamID, profileName})
return nil
}
// Common mock setup
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
team, ok := teamsByName[name]
if !ok {
return nil, &notFoundError{}
}
return team, nil
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
teamsByName[team.Name] = team
return team, nil
}
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
teamsByName[team.Name] = team
return team, nil
}
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables) (fleet.MDMProfilesUpdates, error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) (fleet.MDMProfilesUpdates, error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.HasWindowsUpdateConfigProfileConfiguredFunc = func(ctx context.Context, teamID uint) (bool, error) {
return false, nil
}
t.Run("with values", func(t *testing.T) {
// Reset call trackers
setOrUpdateCalls = nil
deleteCalls = nil
filename := writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
team:
name: Team1
mdm:
windows_updates:
deadline_days: 7
grace_period_days: 2
`)
_ = runAppForTest(t, []string{"apply", "-f", filename})
// Verify SetOrUpdateMDMWindowsConfigProfile was called
require.Len(t, setOrUpdateCalls, 1, "SetOrUpdateMDMWindowsConfigProfile should be called once")
assert.Equal(t, &teamsByName["Team1"].ID, setOrUpdateCalls[0].TeamID)
assert.Equal(t, mdm.FleetWindowsOSUpdatesProfileName, setOrUpdateCalls[0].Name)
assert.NotEmpty(t, setOrUpdateCalls[0].SyncML, "SyncML should contain profile data")
// Verify DeleteMDMWindowsConfigProfileByTeamAndName was NOT called
assert.Empty(t, deleteCalls, "DeleteMDMWindowsConfigProfileByTeamAndName should not be called")
})
t.Run("with null values", func(t *testing.T) {
// Reset call trackers
setOrUpdateCalls = nil
deleteCalls = nil
filename := writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
team:
name: Team1
mdm:
windows_updates:
deadline_days: null
grace_period_days: null
`)
_ = runAppForTest(t, []string{"apply", "-f", filename})
// Verify DeleteMDMWindowsConfigProfileByTeamAndName was called
require.Len(t, deleteCalls, 1, "DeleteMDMWindowsConfigProfileByTeamAndName should be called once")
assert.Equal(t, &teamsByName["Team1"].ID, deleteCalls[0].teamID)
assert.Equal(t, mdm.FleetWindowsOSUpdatesProfileName, deleteCalls[0].name)
// Verify SetOrUpdateMDMWindowsConfigProfile was NOT called
assert.Empty(t, setOrUpdateCalls, "SetOrUpdateMDMWindowsConfigProfile should not be called")
})
t.Run("field omitted", func(t *testing.T) {
// Reset call trackers
setOrUpdateCalls = nil
deleteCalls = nil
filename := writeTmpYml(t, `
apiVersion: v1
kind: fleet
spec:
team:
name: Team1
mdm: {}
`)
_ = runAppForTest(t, []string{"apply", "-f", filename})
// Verify neither function was called
assert.Empty(t, setOrUpdateCalls, "SetOrUpdateMDMWindowsConfigProfile should not be called")
assert.Empty(t, deleteCalls, "DeleteMDMWindowsConfigProfileByTeamAndName should not be called")
})
}
type notFoundError struct{}
var _ fleet.NotFoundError = (*notFoundError)(nil)
func (e *notFoundError) IsNotFound() bool {
return true
}
func (e *notFoundError) Error() string {
return ""
}