<!-- 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 -->
4555 lines
142 KiB
Go
4555 lines
142 KiB
Go
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
}
|
||
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
|
||
// 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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
|
||
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, `Couldn’t 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, `Couldn’t 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: Couldn’t 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: Couldn’t 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, ¬FoundError{}
|
||
}
|
||
|
||
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, ¬FoundError{}
|
||
}
|
||
|
||
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, ¬FoundError{}
|
||
}
|
||
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, ¬FoundError{}
|
||
}
|
||
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 ""
|
||
}
|