diff --git a/assets/images/macos-updates-preview.png b/assets/images/macos-updates-preview.png new file mode 100644 index 0000000000..9bf81c8882 Binary files /dev/null and b/assets/images/macos-updates-preview.png differ diff --git a/assets/images/nudge-screenshot.png b/assets/images/nudge-screenshot.png deleted file mode 100644 index 0db917cf91..0000000000 Binary files a/assets/images/nudge-screenshot.png and /dev/null differ diff --git a/changes/17418-macos-14-nudge b/changes/17418-macos-14-nudge new file mode 100644 index 0000000000..cdf29816b9 --- /dev/null +++ b/changes/17418-macos-14-nudge @@ -0,0 +1 @@ +* macOS 14 and higher no longer display nudge notifications diff --git a/changes/17420-update-ddm-profile-os-updates b/changes/17420-update-ddm-profile-os-updates new file mode 100644 index 0000000000..54188ff7a2 --- /dev/null +++ b/changes/17420-update-ddm-profile-os-updates @@ -0,0 +1 @@ +* Added creation or update of macOS DDM profile to enforce OS Updates settings whenever the settings are changed. diff --git a/changes/issue-17417-ui-os-updates-ddm b/changes/issue-17417-ui-os-updates-ddm new file mode 100644 index 0000000000..06386f9dc6 --- /dev/null +++ b/changes/issue-17417-ui-os-updates-ddm @@ -0,0 +1 @@ +- change UI on OS Updates page to show new nudge for macos DDM diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index b46ac86e99..c0b2796ce7 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -177,6 +177,20 @@ func TestApplyTeamSpecs(t *testing.T) { return nil } + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } + + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } + filename := writeTmpYml(t, ` --- apiVersion: v1 @@ -566,6 +580,24 @@ func TestApplyAppConfig(t *testing.T) { return nil } + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { + return nil + } + + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } + + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } + name := writeTmpYml(t, `--- apiVersion: v1 kind: config @@ -1168,6 +1200,17 @@ func TestApplyAsGitOps(t *testing.T) { ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error { return nil } + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } // Apply global config. name := writeTmpYml(t, `--- diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 160b899857..8fe2d5103a 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/ghodss/yaml" + "github.com/google/uuid" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/pkg/spec" @@ -2219,6 +2220,17 @@ func TestGetTeamsYAMLAndApply(t *testing.T) { ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil } + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } actualYaml := runAppForTest(t, []string{"get", "teams", "--yaml"}) yamlFilePath := writeTmpYml(t, actualYaml) diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 283c7dd32b..4db2ff3b7e 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -14,6 +14,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/service" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -174,6 +175,17 @@ func TestBasicTeamGitOps(t *testing.T) { savedTeam = team return team, nil } + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } var enrolledSecrets []*fleet.EnrollSecret ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { @@ -421,6 +433,17 @@ func TestFullTeamGitOps(t *testing.T) { ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) { return job, nil } + ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { + require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil + } + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } + ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { + return nil + } // Team team := &fleet.Team{ diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 8f523a4d4a..95de007b5f 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -1055,6 +1055,60 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin }, nil } +func (svc *Service) mdmAppleEditedMacOSUpdates(ctx context.Context, teamID *uint, updates fleet.MacOSUpdates) error { + if updates.MinimumVersion.Value == "" { + // OS updates disabled, remove the profile + if err := svc.ds.DeleteMDMAppleDeclarationByName(ctx, teamID, mdm.FleetMacOSUpdatesProfileName); err != nil { + return err + } + var globalOrTeamID uint + if teamID != nil { + globalOrTeamID = *teamID + } + if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{globalOrTeamID}, nil, nil); err != nil { + return ctxerr.Wrap(ctx, err, "bulk set pending host profiles") + } + return nil + } + + // OS updates enabled, create or update the profile with the current settings + + const ( + macOSSoftwareUpdateType = `com.apple.configuration.softwareupdate.enforcement.specific` + macOSSoftwareUpdateIdent = `macos-software-update-94f4bbdf-f439-4fb1-8d27-ae1bb793e105` + ) + rawDecl := []byte(fmt.Sprintf(`{ + "Identifier": %q, + "Type": %q, + "Payload": { + "TargetOSVersion": %q, + "TargetLocalDateTime": "%sT12:00:00" + } +}`, macOSSoftwareUpdateIdent, macOSSoftwareUpdateType, updates.MinimumVersion.Value, updates.Deadline.Value)) + d := fleet.NewMDMAppleDeclaration(rawDecl, teamID, mdm.FleetMacOSUpdatesProfileName, macOSSoftwareUpdateType, macOSSoftwareUpdateIdent) + + // associate the profile with the built-in label that ensures the host is on + // macOS 14+ to receive that profile + lblIDs, err := svc.ds.LabelIDsByName(ctx, []string{fleet.BuiltinLabelMacOS14Plus}) + if err != nil { + return err + } + d.Labels = []fleet.ConfigurationProfileLabel{ + {LabelName: fleet.BuiltinLabelMacOS14Plus, LabelID: lblIDs[fleet.BuiltinLabelMacOS14Plus]}, + } + + decl, err := svc.ds.SetOrUpdateMDMAppleDeclaration(ctx, d) + if err != nil { + return err + } + + // mark all hosts affected by that profile as pending + if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil { + return ctxerr.Wrap(ctx, err, "bulk set pending host declarations") + } + return nil +} + func (svc *Service) mdmWindowsEnableOSUpdates(ctx context.Context, teamID *uint, updates fleet.WindowsUpdates) error { var contents bytes.Buffer params := windowsOSUpdatesProfileOptions{ diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index 3f3db2f215..e394ee0768 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -24,6 +24,7 @@ import ( "github.com/fleetdm/fleet/v4/server/test" "github.com/fleetdm/fleet/v4/server/worker" kitlog "github.com/go-kit/kit/log" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -133,6 +134,9 @@ func TestGetOrCreatePreassignTeam(t *testing.T) { ds.NewJobFuncInvoked = false ds.GetMDMAppleSetupAssistantFuncInvoked = false ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = false + ds.LabelIDsByNameFuncInvoked = false + ds.SetOrUpdateMDMAppleDeclarationFuncInvoked = false + ds.BulkSetPendingMDMHostProfilesFuncInvoked = false } setupDS := func(t *testing.T) { resetInvoked() @@ -183,6 +187,18 @@ func TestGetOrCreatePreassignTeam(t *testing.T) { ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) { return nil, errors.New("not implemented") } + ds.LabelIDsByNameFunc = func(ctx context.Context, names []string) (map[string]uint, error) { + require.Len(t, names, 1) + require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus}) + return map[string]uint{names[0]: 1}, nil + } + ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declaration.DeclarationUUID = uuid.NewString() + return declaration, nil + } + ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error { + return nil + } } authzCtx := &authz_ctx.AuthorizationContext{} diff --git a/ee/server/service/service.go b/ee/server/service/service.go index 526aee13c4..0ba18577ae 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -76,6 +76,7 @@ func NewService( DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage, MDMWindowsEnableOSUpdates: eeservice.mdmWindowsEnableOSUpdates, MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates, + MDMAppleEditedMacOSUpdates: eeservice.mdmAppleEditedMacOSUpdates, }) return eeservice, nil diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index d7dda71261..4aba2df6cd 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -249,6 +249,10 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T return nil, err } if macOSMinVersionUpdated { + if err := svc.mdmAppleEditedMacOSUpdates(ctx, &team.ID, team.Config.MDM.MacOSUpdates); err != nil { + return nil, ctxerr.Wrap(ctx, err, "update DDM profile on macOS updates change") + } + if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), @@ -984,8 +988,10 @@ func (svc *Service) editTeamFromSpec( return err } team.Config.Features = features + var mdmMacOSUpdatesEdited bool if spec.MDM.MacOSUpdates.Deadline.Set || spec.MDM.MacOSUpdates.MinimumVersion.Set { team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates + mdmMacOSUpdatesEdited = true } if spec.MDM.WindowsUpdates.DeadlineDays.Set || spec.MDM.WindowsUpdates.GracePeriodDays.Set { team.Config.MDM.WindowsUpdates = spec.MDM.WindowsUpdates @@ -1165,6 +1171,12 @@ func (svc *Service) editTeamFromSpec( } } + if mdmMacOSUpdatesEdited { + if err := svc.mdmAppleEditedMacOSUpdates(ctx, &team.ID, team.Config.MDM.MacOSUpdates); err != nil { + return err + } + } + return nil } diff --git a/frontend/pages/ManageControlsPage/OSUpdates/OSUpdates.tsx b/frontend/pages/ManageControlsPage/OSUpdates/OSUpdates.tsx index a9d1576406..4e6d7ae311 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/OSUpdates.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/OSUpdates.tsx @@ -17,7 +17,6 @@ import NudgePreview from "./components/NudgePreview"; import TurnOnMdmMessage from "../components/TurnOnMdmMessage/TurnOnMdmMessage"; import CurrentVersionSection from "./components/CurrentVersionSection"; import TargetSection from "./components/TargetSection"; -import { generateKey } from "./components/TargetSection/TargetSection"; export type OSUpdatesSupportedPlatform = "darwin" | "windows"; @@ -38,28 +37,26 @@ const getSelectedPlatform = ( interface IOSUpdates { router: InjectedRouter; - teamIdForApi?: number; + teamIdForApi: number; } const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => { - const { isPremiumTier, setConfig } = useContext(AppContext); + const { isPremiumTier, config, setConfig } = useContext(AppContext); const [ selectedPlatformTab, setSelectedPlatformTab, ] = useState(null); - // FIXME: We're calling this endpoint twice on mount because it also gets called in App.tsx - // whenever the pathname changes. We should find a way to avoid this. const { - data: config, isError: isErrorConfig, isFetching: isFetchingConfig, isLoading: isLoadingConfig, refetch: refetchAppConfig, } = useQuery(["config"], () => configAPI.loadAll(), { refetchOnWindowFocus: false, - onSuccess: (data) => setConfig(data), // update the app context with the fetched config + onSuccess: (data) => setConfig(data), // update the app context with the refetched config + enabled: false, // this is disabled as the config is already fetched in App.tsx }); const { @@ -87,9 +84,6 @@ const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => { ); } - // FIXME: Are these checks still necessary? - if (config === null || teamIdForApi === undefined) return null; - if (isLoadingConfig || isLoadingTeam) return ; // FIXME: Handle error states for app config and team config (need specifications for this). @@ -118,11 +112,7 @@ const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
{ <>

End user experience on macOS

- When a minimum version is saved, the end user sees the below window - until their macOS version is at or above the minimum version. + For macOS 14 and above, end users will see native macOS notifications + (DDM).

-

As the deadline gets closer, Fleet provides stronger encouragement.

+

Everyone else will see the Nudge window.

@@ -33,8 +33,8 @@ const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {

When a Windows host becomes aware of a new update, end users are able to defer restarts. Automatic restarts happen before 8am and after 5pm (end - user’s local time). After the deadline, restarts are forced regardless - of active hours. + user's local time). After the deadline, restarts are forced + regardless of active hours.

{ - return ( - `${args.currentTeamId}-` + - `${getDefaultMacOSDeadline(args)}-` + - `${getDefaultMacOSVersion(args)}-` + - `${getDefaultWindowsDeadlineDays(args)}-` + - `${getDefaultWindowsGracePeriodDays(args)}` - ); -}; - interface ITargetSectionProps { appConfig: IConfig; currentTeamId: number; diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index f8b74da34f..ff7a3b72b8 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -12,6 +12,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + fleetmdm "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" @@ -303,6 +304,20 @@ func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, return nil } +func (ds *Datastore) DeleteMDMAppleDeclarationByName(ctx context.Context, teamID *uint, name string) error { + const stmt = `DELETE FROM mdm_apple_declarations WHERE team_id = ? AND name = ?` + + var globalOrTmID uint + if teamID != nil { + globalOrTmID = *teamID + } + _, err := ds.writer(ctx).ExecContext(ctx, stmt, globalOrTmID, name) + if err != nil { + return ctxerr.Wrap(ctx, err) + } + return nil +} + func (ds *Datastore) deleteMDMAppleDeclaration(ctx context.Context, uuid string) error { stmt := `DELETE FROM mdm_apple_declarations WHERE declaration_uuid = ?` @@ -370,7 +385,7 @@ COALESCE(detail, '') AS detail FROM host_mdm_apple_declarations WHERE -host_uuid = ? AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s'))`, +host_uuid = ? AND declaration_name NOT IN (?) AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s'))`, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryPending, @@ -383,8 +398,13 @@ host_uuid = ? AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', fleet.MDMDeliveryVerified, ) + stmt, args, err := sqlx.In(stmt, hostUUID, hostUUID, fleetmdm.ListFleetReservedMacOSDeclarationNames()) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "building in statement") + } + var profiles []fleet.HostMDMAppleProfile - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, hostUUID, hostUUID); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, args...); err != nil { return nil, err } return profiles, nil @@ -2180,7 +2200,6 @@ func subqueryAppleProfileStatus(status fleet.MDMDeliveryStatus) (string, []any, arg := map[string]any{ "install": fleet.MDMOperationTypeInstall, - "remove": fleet.MDMOperationTypeRemove, "verifying": fleet.MDMDeliveryVerifying, "failed": fleet.MDMDeliveryFailed, "verified": fleet.MDMDeliveryVerified, @@ -2205,7 +2224,9 @@ func subqueryAppleDeclarationStatus() (string, []any, error) { host_mdm_apple_declarations d1 WHERE h.uuid = d1.host_uuid - AND d1.status = :failed) THEN + AND d1.operation_type = :install + AND d1.status = :failed + AND d1.declaration_name NOT IN (:reserved_names)) THEN 'declarations_failed' WHEN EXISTS ( SELECT @@ -2214,8 +2235,10 @@ func subqueryAppleDeclarationStatus() (string, []any, error) { host_mdm_apple_declarations d2 WHERE h.uuid = d2.host_uuid + AND d2.operation_type = :install AND(d2.status IS NULL OR d2.status = :pending) + AND d2.declaration_name NOT IN (:reserved_names) AND NOT EXISTS ( SELECT 1 @@ -2223,7 +2246,9 @@ func subqueryAppleDeclarationStatus() (string, []any, error) { host_mdm_apple_declarations d3 WHERE h.uuid = d3.host_uuid - AND d3.status = :failed)) THEN + AND d3.operation_type = :install + AND d3.status = :failed + AND d3.declaration_name NOT IN (:reserved_names))) THEN 'declarations_pending' WHEN EXISTS ( SELECT @@ -2232,13 +2257,17 @@ func subqueryAppleDeclarationStatus() (string, []any, error) { host_mdm_apple_declarations d4 WHERE h.uuid = d4.host_uuid + AND d4.operation_type = :install AND d4.status = :verifying + AND d4.declaration_name NOT IN (:reserved_names) AND NOT EXISTS ( SELECT 1 FROM host_mdm_apple_declarations d5 WHERE (h.uuid = d5.host_uuid + AND d5.operation_type = :install + AND d5.declaration_name NOT IN (:reserved_names) AND(d5.status IS NULL OR d5.status IN(:pending, :failed))))) THEN 'declarations_verifying' @@ -2249,13 +2278,17 @@ func subqueryAppleDeclarationStatus() (string, []any, error) { host_mdm_apple_declarations d6 WHERE h.uuid = d6.host_uuid + AND d6.operation_type = :install AND d6.status = :verified + AND d6.declaration_name NOT IN (:reserved_names) AND NOT EXISTS ( SELECT 1 FROM host_mdm_apple_declarations d7 WHERE (h.uuid = d7.host_uuid + AND d7.operation_type = :install + AND d7.declaration_name NOT IN (:reserved_names) AND(d7.status IS NULL OR d7.status IN(:pending, :failed, :verifying))))) THEN 'declarations_verified' @@ -2263,19 +2296,22 @@ func subqueryAppleDeclarationStatus() (string, []any, error) { '' END` - // TODO: do we need to differentiate between install and remove? arg := map[string]any{ - // "install": fleet.MDMOperationTypeInstall, - // "remove": fleet.MDMOperationTypeRemove, - "verifying": fleet.MDMDeliveryVerifying, - "failed": fleet.MDMDeliveryFailed, - "verified": fleet.MDMDeliveryVerified, - "pending": fleet.MDMDeliveryPending, + "install": fleet.MDMOperationTypeInstall, + "verifying": fleet.MDMDeliveryVerifying, + "failed": fleet.MDMDeliveryFailed, + "verified": fleet.MDMDeliveryVerified, + "pending": fleet.MDMDeliveryPending, + "reserved_names": fleetmdm.ListFleetReservedMacOSDeclarationNames(), } query, args, err := sqlx.Named(declNamedStmt, arg) if err != nil { return "", nil, fmt.Errorf("subqueryAppleDeclarationStatus: %w", err) } + query, args, err = sqlx.In(query, args...) + if err != nil { + return "", nil, fmt.Errorf("subqueryAppleDeclarationStatus resolve IN: %w", err) + } return query, args, nil } @@ -3398,6 +3434,7 @@ ON DUPLICATE KEY UPDATE uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()), checksum = VALUES(checksum), name = VALUES(name), + identifier = VALUES(identifier), raw_json = VALUES(raw_json) ` @@ -3407,18 +3444,18 @@ DELETE FROM WHERE team_id = ? AND %s ` - andIdentNotInList := "identifier NOT IN (?)" // added to fmtDeleteStmt if needed + andNameNotInList := "name NOT IN (?)" // added to fmtDeleteStmt if needed const loadExistingDecls = ` SELECT - identifier, + name, declaration_uuid, raw_json FROM mdm_apple_declarations WHERE team_id = ? AND - identifier IN (?) + name IN (?) ` var declTeamID uint @@ -3426,22 +3463,22 @@ WHERE declTeamID = *tmID } - // build a list of identifiers for the incoming declarations, will keep the + // build a list of names for the incoming declarations, will keep the // existing ones if there's a match and no change - incomingIdents := make([]string, len(incomingDeclarations)) - // at the same time, index the incoming declarations keyed by identifier for ease + incomingNames := make([]string, len(incomingDeclarations)) + // at the same time, index the incoming declarations keyed by name for ease // or processing incomingDecls := make(map[string]*fleet.MDMAppleDeclaration, len(incomingDeclarations)) for i, p := range incomingDeclarations { - incomingIdents[i] = p.Identifier - incomingDecls[p.Identifier] = p + incomingNames[i] = p.Name + incomingDecls[p.Name] = p } var existingDecls []*fleet.MDMAppleDeclaration - if len(incomingIdents) > 0 { - // load existing declarations that match the incoming declarations by identifiers - stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingIdents) + if len(incomingNames) > 0 { + // load existing declarations that match the incoming declarations by names + stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingNames) if err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "inselect") { // TODO(JVE): do we need to create similar errors for testing decls? if err == nil { err = errors.New(ds.testBatchSetMDMAppleProfilesErr) @@ -3457,28 +3494,23 @@ WHERE } // figure out if we need to delete any declarations - keepIdents := make([]any, 0, len(incomingIdents)) + keepNames := make([]string, 0, len(incomingNames)) for _, p := range existingDecls { - if newP := incomingDecls[p.Identifier]; newP != nil { - keepIdents = append(keepIdents, p.Identifier) + if newP := incomingDecls[p.Name]; newP != nil { + keepNames = append(keepNames, p.Name) } } + keepNames = append(keepNames, fleetmdm.ListFleetReservedMacOSDeclarationNames()...) var delArgs []any var delStmt string - if len(keepIdents) == 0 { + if len(keepNames) == 0 { // delete all declarations for the team delStmt = fmt.Sprintf(fmtDeleteStmt, "TRUE") delArgs = []any{declTeamID} } else { - // delete the obsolete declarations (all those that are not in keepIdents) - stmt, args, err := sqlx.In(fmt.Sprintf(fmtDeleteStmt, andIdentNotInList), declTeamID, keepIdents) - // if err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "inselect") { // TODO(JVE): do we need to create similar errors for testing decls? - // if err == nil { - // err = errors.New(ds.testBatchSetMDMAppleProfilesErr) - // } - // return nil, ctxerr.Wrap(ctx, err, "build query to load existing declarations") - // } + // delete the obsolete declarations (all those that are not in keepNames) + stmt, args, err := sqlx.In(fmt.Sprintf(fmtDeleteStmt, andNameNotInList), declTeamID, keepNames) if err != nil { return nil, ctxerr.Wrap(ctx, err, "build query to delete obsolete profiles") } @@ -3511,7 +3543,7 @@ WHERE } incomingLabels := []fleet.ConfigurationProfileLabel{} - if len(incomingIdents) > 0 { + if len(incomingNames) > 0 { var newlyInsertedDecls []*fleet.MDMAppleDeclaration // load current declarations (again) that match the incoming declarations by name to grab their uuids // this is an easy way to grab the identifiers for both the existing declarations and the new ones we generated. @@ -3520,7 +3552,7 @@ WHERE // information without this extra request in the previous DB // calls. Due to time constraints, I'm leaving that // optimization for a later iteration. - stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingIdents) + stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingNames) if err != nil { return nil, ctxerr.Wrap(ctx, err, "build query to load newly inserted declarations") } @@ -3529,9 +3561,9 @@ WHERE } for _, newlyInsertedDecl := range newlyInsertedDecls { - incomingDecl, ok := incomingDecls[newlyInsertedDecl.Identifier] + incomingDecl, ok := incomingDecls[newlyInsertedDecl.Name] if !ok { - return nil, ctxerr.Wrapf(ctx, err, "declaration %q is in the database but was not incoming", newlyInsertedDecl.Identifier) + return nil, ctxerr.Wrapf(ctx, err, "declaration %q is in the database but was not incoming", newlyInsertedDecl.Name) } for _, label := range incomingDecl.Labels { @@ -3552,10 +3584,7 @@ WHERE } func (ds *Datastore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { - declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString() - checksum := md5ChecksumScriptContent(string(declaration.RawJSON)) - - stmt := ` + const stmt = ` INSERT INTO mdm_apple_declarations ( declaration_uuid, team_id, @@ -3572,13 +3601,48 @@ INSERT INTO mdm_apple_declarations ( ) )` + return ds.insertOrUpsertMDMAppleDeclaration(ctx, stmt, declaration) +} + +func (ds *Datastore) SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + const stmt = ` +INSERT INTO mdm_apple_declarations ( + declaration_uuid, + team_id, + identifier, + name, + raw_json, + checksum, + uploaded_at) +(SELECT ?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP() FROM DUAL WHERE + NOT EXISTS ( + SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ? + ) AND NOT EXISTS ( + SELECT 1 FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ? + ) +) +ON DUPLICATE KEY UPDATE + identifier = VALUES(identifier), + uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()), + raw_json = VALUES(raw_json), + checksum = VALUES(checksum)` + + return ds.insertOrUpsertMDMAppleDeclaration(ctx, stmt, declaration) +} + +func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insOrUpsertStmt string, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString() + checksum := md5ChecksumScriptContent(string(declaration.RawJSON)) + var tmID uint if declaration.TeamID != nil { tmID = *declaration.TeamID } + const reloadStmt = `SELECT declaration_uuid FROM mdm_apple_declarations WHERE name = ? AND team_id = ?` + err := ds.withTx(ctx, func(tx sqlx.ExtContext) error { - res, err := tx.ExecContext(ctx, stmt, + res, err := tx.ExecContext(ctx, insOrUpsertStmt, declUUID, tmID, declaration.Identifier, declaration.Name, declaration.RawJSON, checksum, declaration.Name, tmID, declaration.Name, tmID) if err != nil { switch { @@ -3598,6 +3662,10 @@ INSERT INTO mdm_apple_declarations ( } } + if err := sqlx.GetContext(ctx, tx, &declUUID, reloadStmt, declaration.Name, tmID); err != nil { + return ctxerr.Wrap(ctx, err, "reload apple mdm declaration") + } + for i := range declaration.Labels { declaration.Labels[i].ProfileUUID = declUUID } diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 031b7017ca..9ba8fe8b2e 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -71,6 +71,7 @@ func TestMDMApple(t *testing.T) { {"ScreenDEPAssignProfileSerialsForCooldown", testScreenDEPAssignProfileSerialsForCooldown}, {"MDMAppleDDMDeclarationsToken", testMDMAppleDDMDeclarationsToken}, {"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs}, + {"SetOrUpdateMDMAppleDeclaration", testSetOrUpdateMDMAppleDDMDeclaration}, {"DEPAssignmentUpdates", testMDMAppleDEPAssignmentUpdates}, } @@ -332,6 +333,27 @@ func testDeleteMDMAppleConfigProfile(t *testing.T, ds *Datastore) { err = ds.DeleteMDMAppleConfigProfile(ctx, initialCP.ProfileUUID) require.ErrorIs(t, err, sql.ErrNoRows) + + // delete by name via a non-existing name is not an error + err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, "test") + require.NoError(t, err) + + testDecl := declForTest("D1", "D1", "{}") + dbDecl, err := ds.NewMDMAppleDeclaration(ctx, testDecl) + require.NoError(t, err) + + // delete for a non-existing team does nothing + err = ds.DeleteMDMAppleDeclarationByName(ctx, ptr.Uint(1), dbDecl.Name) + require.NoError(t, err) + // ddm still exists + _, err = ds.GetMDMAppleDeclaration(ctx, dbDecl.DeclarationUUID) + require.NoError(t, err) + + // properly delete + err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, dbDecl.Name) + require.NoError(t, err) + _, err = ds.GetMDMAppleDeclaration(ctx, dbDecl.DeclarationUUID) + require.ErrorIs(t, err, sql.ErrNoRows) } func testDeleteMDMAppleConfigProfileByTeamAndIdentifier(t *testing.T, ds *Datastore) { @@ -4787,6 +4809,115 @@ func testMDMAppleSetPendingDeclarationsAs(t *testing.T, ds *Datastore) { checkStatus(profs, fleet.MDMDeliveryFailed, "mock error") } +func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) { + ctx := context.Background() + l1, err := ds.NewLabel(ctx, &fleet.Label{Name: "l1", Query: "select 1"}) + require.NoError(t, err) + l2, err := ds.NewLabel(ctx, &fleet.Label{Name: "l2", Query: "select 2"}) + require.NoError(t, err) + tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: "tm1"}) + require.NoError(t, err) + + d1, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1", + Name: "d1", + RawJSON: json.RawMessage(`{"Identifier": "i1"}`), + }) + require.NoError(t, err) + + // try to create same name, different identifier fails + _, err = ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1b", + Name: "d1", + RawJSON: json.RawMessage(`{"Identifier": "i1b"}`), + }) + require.Error(t, err) + var existsErr *existsError + require.ErrorAs(t, err, &existsErr) + + // try to create different name, same identifier fails + _, err = ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1", + Name: "d1b", + RawJSON: json.RawMessage(`{"Identifier": "i1"}`), + }) + require.Error(t, err) + require.ErrorAs(t, err, &existsErr) + + // create same declaration for a different team works + d1tm1, err := ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1", + Name: "d1", + TeamID: &tm1.ID, + RawJSON: json.RawMessage(`{"Identifier": "i1"}`), + }) + require.NoError(t, err) + require.NotEqual(t, d1.DeclarationUUID, d1tm1.DeclarationUUID) + + d1Ori, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID) + require.NoError(t, err) + require.Empty(t, d1Ori.Labels) + + // update d1 with different identifier and labels + d1, err = ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1b", + Name: "d1", + RawJSON: json.RawMessage(`{"Identifier": "i1b"}`), + Labels: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}}, + }) + require.NoError(t, err) + require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID) + require.NotEqual(t, d1.DeclarationUUID, d1tm1.DeclarationUUID) + + d1B, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID) + require.NoError(t, err) + require.Len(t, d1B.Labels, 1) + require.Equal(t, l1.ID, d1B.Labels[0].LabelID) + + // update d1 with different label + d1, err = ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1b", + Name: "d1", + RawJSON: json.RawMessage(`{"Identifier": "i1b"}`), + Labels: []fleet.ConfigurationProfileLabel{{LabelName: l2.Name, LabelID: l2.ID}}, + }) + require.NoError(t, err) + require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID) + + d1C, err := ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID) + require.NoError(t, err) + require.Len(t, d1C.Labels, 1) + require.Equal(t, l2.ID, d1C.Labels[0].LabelID) + + // update d1tm1 with different identifier and label + d1tm1B, err := ds.SetOrUpdateMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{ + Identifier: "i1b", + Name: "d1", + TeamID: &tm1.ID, + RawJSON: json.RawMessage(`{"Identifier": "i1b"}`), + Labels: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}}, + }) + require.NoError(t, err) + require.Equal(t, d1tm1B.DeclarationUUID, d1tm1.DeclarationUUID) + + d1tm1B, err = ds.GetMDMAppleDeclaration(ctx, d1tm1B.DeclarationUUID) + require.NoError(t, err) + require.Len(t, d1tm1B.Labels, 1) + require.Equal(t, l1.ID, d1tm1B.Labels[0].LabelID) + + // delete no-team d1 + err = ds.DeleteMDMAppleDeclarationByName(ctx, nil, "d1") + require.NoError(t, err) + + // it does not exist anymore, but the tm1 one still does + _, err = ds.GetMDMAppleDeclaration(ctx, d1.DeclarationUUID) + require.Error(t, err) + + d1tm1B, err = ds.GetMDMAppleDeclaration(ctx, d1tm1B.DeclarationUUID) + require.NoError(t, err) + require.Equal(t, d1tm1B.DeclarationUUID, d1tm1.DeclarationUUID) +} + func TestMDMAppleProfileVerification(t *testing.T) { ds := CreateMySQLDS(t) ctx := context.Background() diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index 4debc4c0f6..cba41dbcc0 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -70,6 +70,8 @@ func TestLabels(t *testing.T) { {"ListHostsInLabelOSSettings", testLabelsListHostsInLabelOSSettings}, {"AddDeleteLabelsToFromHost", testAddDeleteLabelsToFromHost}, } + // call TruncateTables first to remove migration-created labels + TruncateTables(t, ds) for _, c := range cases { t.Run(c.name, func(t *testing.T) { defer TruncateTables(t, ds) @@ -93,15 +95,13 @@ func testLabelsAddAllHosts(deferred bool, t *testing.T, db *Datastore) { err = db.UpdateHost(context.Background(), host) require.NoError(t, err) - // No labels to check queries, err := db.LabelQueriesForHost(context.Background(), host) assert.Nil(t, err) assert.Len(t, queries, 0) - // Only 'All Hosts' label should be returned labels, err := db.ListLabelsForHost(context.Background(), host.ID) assert.Nil(t, err) - assert.Len(t, labels, 1) + assert.Len(t, labels, 1) // all hosts only newLabels := []*fleet.LabelSpec{ // Note these are intentionally out of order @@ -1537,3 +1537,14 @@ func testAddDeleteLabelsToFromHost(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Empty(t, labels) } + +func labelIDFromName(t *testing.T, ds fleet.Datastore, name string) uint { + allLbls, err := ds.ListLabels(context.Background(), fleet.TeamFilter{User: test.UserAdmin}, fleet.ListOptions{}) + require.Nil(t, err) + for _, lbl := range allLbls { + if lbl.Name == name { + return lbl.ID + } + } + return 0 +} diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index ef16b993e8..2ace200a05 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -168,7 +168,7 @@ FROM ( WHERE team_id = ? AND name NOT IN (?) - + UNION SELECT @@ -181,7 +181,8 @@ FROM ( created_at, uploaded_at FROM mdm_apple_declarations - WHERE team_id = ? + WHERE team_id = ? AND + name NOT IN (?) ) as combined_profiles ` @@ -201,7 +202,7 @@ FROM ( fleetNames = append(fleetNames, k) } - args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID} + args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID, fleetNames} stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt) stmt, args, err := sqlx.In(stmt, args...) @@ -268,7 +269,7 @@ FROM WHERE mcpl.apple_profile_uuid IN (?) OR mcpl.windows_profile_uuid IN (?) -UNION ALL +UNION ALL SELECT apple_declaration_uuid as profile_uuid, label_name, @@ -315,9 +316,10 @@ func (ds *Datastore) BulkSetPendingMDMHostProfiles( profileUUIDs, hostUUIDs []string, ) error { var ( - countArgs int - macProfUUIDs []string - winProfUUIDs []string + countArgs int + macProfUUIDs []string + winProfUUIDs []string + hasAppleDecls bool ) if len(hostIDs) > 0 { @@ -331,9 +333,14 @@ func (ds *Datastore) BulkSetPendingMDMHostProfiles( // split into mac and win profiles for _, puid := range profileUUIDs { - if strings.HasPrefix(puid, "a") { + if strings.HasPrefix(puid, fleet.MDMAppleProfileUUIDPrefix) { macProfUUIDs = append(macProfUUIDs, puid) + } else if strings.HasPrefix(puid, fleet.MDMAppleDeclarationUUIDPrefix) { + hasAppleDecls = true } else { + // Note: defaulting to windows profiles without checking the prefix as + // many tests fail otherwise and it's a whole rabbit hole that I can't + // address at the moment. winProfUUIDs = append(winProfUUIDs, puid) } } @@ -347,8 +354,19 @@ func (ds *Datastore) BulkSetPendingMDMHostProfiles( if countArgs == 0 { return nil } - if len(macProfUUIDs) > 0 && len(winProfUUIDs) > 0 { - return errors.New("profile uuids must all be Apple or Windows profiles") + + var countProfUUIDs int + if len(macProfUUIDs) > 0 { + countProfUUIDs++ + } + if len(winProfUUIDs) > 0 { + countProfUUIDs++ + } + if hasAppleDecls { + countProfUUIDs++ + } + if countProfUUIDs > 1 { + return errors.New("profile uuids must be all Apple profiles, all Apple declarations, or all Windows profiles") } var ( @@ -416,7 +434,7 @@ WHERE return ds.withTx(ctx, func(tx sqlx.ExtContext) error { // TODO: this could be optimized to avoid querying for platform when // profileIDs or profileUUIDs are provided. - if len(hosts) == 0 { + if len(hosts) == 0 && !hasAppleDecls { uuidStmt, args, err := sqlx.In(uuidStmt, args...) if err != nil { return ctxerr.Wrap(ctx, err, "prepare query to load host UUIDs") diff --git a/server/datastore/mysql/migrations/tables/20240415104633_CreateMacOSSonomaBuiltinLabel.go b/server/datastore/mysql/migrations/tables/20240415104633_CreateMacOSSonomaBuiltinLabel.go new file mode 100644 index 0000000000..23ac00531b --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240415104633_CreateMacOSSonomaBuiltinLabel.go @@ -0,0 +1,62 @@ +package tables + +import ( + "database/sql" + "fmt" + "time" + + "github.com/VividCortex/mysqlerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/go-sql-driver/mysql" +) + +func init() { + MigrationClient.AddMigration(Up_20240415104633, Down_20240415104633) +} + +func Up_20240415104633(tx *sql.Tx) error { + const stmt = ` + INSERT INTO labels ( + name, + description, + query, + platform, + label_type, + label_membership_type, + created_at, + updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) +` + + // hard-coded timestamps are used so that schema.sql is stable + ts := time.Date(2024, 4, 3, 0, 0, 0, 0, time.UTC) + _, err := tx.Exec( + stmt, + fleet.BuiltinLabelMacOS14Plus, + "macOS hosts with version 14 and above", + `select 1 from os_version where platform = 'darwin' and major >= 14;`, + "darwin", + fleet.LabelTypeBuiltIn, + fleet.LabelMembershipTypeDynamic, + ts, + ts, + ) + if err != nil { + if driverErr, ok := err.(*mysql.MySQLError); ok { + if driverErr.Number == mysqlerr.ER_DUP_ENTRY { + // TODO(mna): how do we feel about this approach to ensure the new + // Fleet-reserved name is unique? All label names need to be unique + // across built-in and regular. (I don't think we've done anything + // special before, but this seems a bit nicer/clearer as to why the + // migration may have failed and how to fix it) + return fmt.Errorf("a label with the name %q already exists, please rename it before applying this migration: %w", fleet.BuiltinLabelMacOS14Plus, err) + } + } + return err + } + return nil +} + +func Down_20240415104633(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240415104633_CreateMacOSSonomaBuiltinLabel_test.go b/server/datastore/mysql/migrations/tables/20240415104633_CreateMacOSSonomaBuiltinLabel_test.go new file mode 100644 index 0000000000..153b24ed32 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240415104633_CreateMacOSSonomaBuiltinLabel_test.go @@ -0,0 +1,29 @@ +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20240415104633(t *testing.T) { + db := applyUpToPrev(t) + + execNoErr(t, db, "INSERT INTO labels (name, query, platform) VALUES (?,?,?)", "NOT macOS 14+ (Sonoma+)", "SELECT 1", "windows") + + // Apply current migration. + // + // The case where the name already exists could not be tested because + // applying the next migration fails drastically when the migration returns + // an error (it calls log.Fatal) and the test cannot continue after the + // error, but it has been tested manually. + applyNext(t, db) + + var names []string + err := db.Select(&names, `SELECT name FROM labels`) + require.NoError(t, err) + + require.GreaterOrEqual(t, len(names), 2) + require.Contains(t, names, "macOS 14+ (Sonoma+)") + require.Contains(t, names, "NOT macOS 14+ (Sonoma+)") +} diff --git a/server/datastore/mysql/operating_systems.go b/server/datastore/mysql/operating_systems.go index 50bab6e30e..4b9fd0a2d6 100644 --- a/server/datastore/mysql/operating_systems.go +++ b/server/datastore/mysql/operating_systems.go @@ -45,6 +45,10 @@ func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, }) } +func (ds *Datastore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return getHostOperatingSystemDB(ctx, ds.reader(ctx), hostID) +} + // getOrGenerateOperatingSystemDB queries the `operating_systems` table with the // name, version, arch, and kernel_version of the given operating system. If found, // it returns the record including the associated ID. If not found, it returns a call @@ -168,11 +172,11 @@ func getIDHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID // getIDHostOperatingSystemDB queries the `operating_systems` table and returns the // operating system record associated with the given host ID based on a subquery // of the `host_operating_system` table. -func getHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint) (*fleet.OperatingSystem, error) { +func getHostOperatingSystemDB(ctx context.Context, tx sqlx.QueryerContext, hostID uint) (*fleet.OperatingSystem, error) { var os fleet.OperatingSystem stmt := "SELECT id, name, version, arch, kernel_version, platform, display_version, os_version_id FROM operating_systems WHERE id = (SELECT os_id FROM host_operating_system WHERE host_id = ?)" if err := sqlx.GetContext(ctx, tx, &os, stmt, hostID); err != nil { - return nil, err + return nil, ctxerr.Wrap(ctx, err, "getting host os") } return &os, nil } diff --git a/server/datastore/mysql/operating_systems_test.go b/server/datastore/mysql/operating_systems_test.go index f8bc2aa2be..5819d269a7 100644 --- a/server/datastore/mysql/operating_systems_test.go +++ b/server/datastore/mysql/operating_systems_test.go @@ -3,6 +3,7 @@ package mysql import ( "context" "database/sql" + "errors" "fmt" "sync" "testing" @@ -295,6 +296,9 @@ func TestGetHostOperatingSystem(t *testing.T) { _, err = getHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID) require.ErrorIs(t, err, sql.ErrNoRows) + _, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.ErrorIs(t, err, sql.ErrNoRows) + // insert test host and os id err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[0].ID) require.NoError(t, err) @@ -302,6 +306,10 @@ func TestGetHostOperatingSystem(t *testing.T) { require.NoError(t, err) require.Equal(t, osList[0], *os) + os, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.NoError(t, err) + require.Equal(t, osList[0], *os) + // update test host with new os id err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[1].ID) require.NoError(t, err) @@ -309,12 +317,20 @@ func TestGetHostOperatingSystem(t *testing.T) { require.NoError(t, err) require.Equal(t, osList[1], *os) + os, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1], *os) + // no change err = upsertHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID, osList[1].ID) require.NoError(t, err) os, err = getHostOperatingSystemDB(ctx, ds.writer(ctx), testHostID) require.NoError(t, err) require.Equal(t, osList[1], *os) + + os, err = ds.GetHostOperatingSystem(ctx, testHostID) + require.NoError(t, err) + require.Equal(t, osList[1], *os) } func TestCleanupHostOperatingSystems(t *testing.T) { @@ -352,7 +368,7 @@ func TestCleanupHostOperatingSystems(t *testing.T) { assertDeletedHostOS := func(expectDeletedIDs []uint) { for _, h := range testHosts { os, err := getHostOperatingSystemDB(ctx, ds.writer(ctx), h.ID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { require.Contains(t, expectDeletedIDs, h.ID) return } diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 08da189437..19df87045d 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -634,8 +634,9 @@ CREATE TABLE `labels` ( PRIMARY KEY (`id`), UNIQUE KEY `idx_label_unique_name` (`name`), FULLTEXT KEY `labels_search` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +INSERT INTO `labels` VALUES (1,'2024-04-03 00:00:00','2024-04-03 00:00:00','macOS 14+ (Sonoma+)','macOS hosts with version 14 and above','select 1 from os_version where platform = \'darwin\' and major >= 14;','darwin',1,0); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `locks` ( @@ -885,9 +886,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=263 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=264 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/datastore/mysql/statistics_test.go b/server/datastore/mysql/statistics_test.go index d7648c7859..7a151d6527 100644 --- a/server/datastore/mysql/statistics_test.go +++ b/server/datastore/mysql/statistics_test.go @@ -12,6 +12,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -50,6 +51,11 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { premiumLicense := &fleet.LicenseInfo{Tier: fleet.TierPremium, Organization: "Fleet"} freeLicense := &fleet.LicenseInfo{Tier: fleet.TierFree} + var builtinLabels int + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(ctx, q, &builtinLabels, `SELECT COUNT(*) FROM labels`) + }) + // First time running with no hosts stats, shouldSend, err := ds.ShouldSendStatistics(license.NewContext(ctx, premiumLicense), time.Millisecond, fleetConfig) require.NoError(t, err) @@ -60,7 +66,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { assert.Equal(t, 0, stats.NumUsers) assert.Equal(t, 0, stats.NumTeams) assert.Equal(t, 0, stats.NumPolicies) - assert.Equal(t, 0, stats.NumLabels) + assert.Equal(t, builtinLabels, stats.NumLabels) assert.Equal(t, false, stats.SoftwareInventoryEnabled) assert.Equal(t, true, stats.SystemUsersEnabled) assert.Equal(t, false, stats.VulnDetectionEnabled) @@ -198,7 +204,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) { assert.Equal(t, 2, stats.NumUsers) assert.Equal(t, 1, stats.NumTeams) assert.Equal(t, 1, stats.NumPolicies) - assert.Equal(t, 1, stats.NumLabels) + assert.Equal(t, builtinLabels+1, stats.NumLabels) assert.Equal(t, false, stats.SoftwareInventoryEnabled) assert.Equal(t, false, stats.SystemUsersEnabled) assert.Equal(t, false, stats.VulnDetectionEnabled) diff --git a/server/datastore/mysql/targets_test.go b/server/datastore/mysql/targets_test.go index 411fa0c2e1..d6ed1effd5 100644 --- a/server/datastore/mysql/targets_test.go +++ b/server/datastore/mysql/targets_test.go @@ -76,16 +76,16 @@ func testTargetsCountHosts(t *testing.T, ds *Datastore) { h6 := initHost(mockClock.Now().Add(thirtyDaysAndAMinuteAgo*time.Minute), 3600, 3600, nil) l1 := fleet.LabelSpec{ - ID: 1, Name: "label foo", Query: "query foo", } l2 := fleet.LabelSpec{ - ID: 2, Name: "label bar", Query: "query bar", } require.NoError(t, ds.ApplyLabelSpecs(context.Background(), []*fleet.LabelSpec{&l1, &l2})) + l1.ID = labelIDFromName(t, ds, l1.Name) + l2.ID = labelIDFromName(t, ds, l2.Name) for _, h := range []*fleet.Host{h1, h2, h3, h6} { err = ds.RecordLabelQueryExecutions(context.Background(), h, map[uint]*bool{l1.ID: ptr.Bool(true)}, mockClock.Now(), false) diff --git a/server/datastore/mysql/unicode_test.go b/server/datastore/mysql/unicode_test.go index f831780612..360a7771eb 100644 --- a/server/datastore/mysql/unicode_test.go +++ b/server/datastore/mysql/unicode_test.go @@ -18,12 +18,12 @@ func TestUnicode(t *testing.T) { defer ds.Close() l1 := fleet.LabelSpec{ - ID: 1, Name: "測試", Query: "query foo", } err := ds.ApplyLabelSpecs(context.Background(), []*fleet.LabelSpec{&l1}) require.Nil(t, err) + l1.ID = labelIDFromName(t, ds, l1.Name) label, _, err := ds.Label(context.Background(), l1.ID) require.Nil(t, err) diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 97708a98f7..71de52073a 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -595,7 +595,7 @@ func (r *MDMAppleRawDeclaration) ValidateUserProvided() error { // Check against types we don't allow if r.Type == `com.apple.configuration.softwareupdate.enforcement.specific` { - return NewInvalidArgumentError(r.Type, "Declaration profile can’t include OS updates settings. OS updates coming soon!") + return NewInvalidArgumentError(r.Type, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.") } if _, forbidden := ForbiddenDeclTypes[r.Type]; forbidden { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index c241fc4122..97f66e6c7f 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -545,6 +545,9 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // OperatingSystemsStore + // GetHostOperatingSystem returns the operating system information + // for a given host. + GetHostOperatingSystem(ctx context.Context, hostID uint) (*OperatingSystem, error) // ListOperationsSystems returns all operating systems (id, name, version) ListOperatingSystems(ctx context.Context) ([]OperatingSystem, error) // ListOperatingSystemsForPlatform returns all operating systems for the given platform. @@ -966,6 +969,10 @@ type Datastore interface { // to the specified profile uuid. DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error + // DeleteMDMAppleDeclartionByName deletes a DDM profile by its name for the + // specified team (or no team). + DeleteMDMAppleDeclarationByName(ctx context.Context, teamID *uint, name string) error + BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context, payload []*MDMAppleProfilePayload) error // DeleteMDMAppleConfigProfileByTeamAndIdentifier deletes a configuration @@ -1350,6 +1357,9 @@ type Datastore interface { // NewMDMAppleDeclaration creates and returns a new MDM Apple declaration. NewMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error) + // SetOrUpdateMDMAppleDeclaration upserts the MDM Apple declaration. + SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error) + /////////////////////////////////////////////////////////////////////////////// // Host Script Results diff --git a/server/fleet/labels.go b/server/fleet/labels.go index 977eca44de..a40c53049e 100644 --- a/server/fleet/labels.go +++ b/server/fleet/labels.go @@ -155,6 +155,7 @@ const ( BuiltinLabelNameRedHatLinux = "Red Hat Linux" BuiltinLabelNameAllLinux = "All Linux" BuiltinLabelNameChrome = "chrome" + BuiltinLabelMacOS14Plus = "macOS 14+ (Sonoma+)" ) // ReservedLabelNames returns a map of label name strings @@ -169,5 +170,6 @@ func ReservedLabelNames() map[string]struct{} { BuiltinLabelNameRedHatLinux: {}, BuiltinLabelNameAllLinux: {}, BuiltinLabelNameChrome: {}, + BuiltinLabelMacOS14Plus: {}, } } diff --git a/server/fleet/operating_systems.go b/server/fleet/operating_systems.go index 19402c1633..91c9a1ead7 100644 --- a/server/fleet/operating_systems.go +++ b/server/fleet/operating_systems.go @@ -1,6 +1,10 @@ package fleet -import "strings" +import ( + "fmt" + "strconv" + "strings" +) // OperatingSystem is an operating system uniquely identified according to its name and version. type OperatingSystem struct { @@ -27,3 +31,23 @@ type OperatingSystem struct { func (os OperatingSystem) IsWindows() bool { return strings.ToLower(os.Platform) == "windows" } + +// RequiresNudge returns whether the target platform is darwin and +// below version 14. Starting at macOS 14 nudge is no longer required, +// as the mechanism to notify users about updates is built in. +func (os *OperatingSystem) RequiresNudge() (bool, error) { + if os.Platform != "darwin" { + return false, nil + } + + versionFloat, err := strconv.ParseFloat(os.Version, 32) + if err != nil { + return false, fmt.Errorf("parsing macos version \"%s\": %w", os.Version, err) + } + + if float32(versionFloat) < 14 { + return true, nil + } + + return false, nil +} diff --git a/server/fleet/operating_systems_test.go b/server/fleet/operating_systems_test.go index ea981c6175..3f737297b2 100644 --- a/server/fleet/operating_systems_test.go +++ b/server/fleet/operating_systems_test.go @@ -21,3 +21,35 @@ func TestOperatingSystemIsWindows(t *testing.T) { require.Equal(t, tc.isWindows, sut.IsWindows()) } } + +func TestOperatingSystemRequiresNudge(t *testing.T) { + testCases := []struct { + platform string + version string + requiresNudge bool + parseError bool + }{ + {platform: "chrome"}, + {platform: "chrome", version: "12.1"}, + {platform: "chrome", version: "15"}, + {platform: "darwin", parseError: true}, + {platform: "darwin", version: "12.0", requiresNudge: true}, + {platform: "darwin", version: "11", requiresNudge: true}, + {platform: "darwin", version: "14.0"}, + {platform: "darwin", version: "14.3"}, + {platform: "windows"}, + {platform: "windows", version: "12.2"}, + {platform: "windows", version: "15.4"}, + } + + for _, tc := range testCases { + os := OperatingSystem{Platform: tc.platform, Version: tc.version} + req, err := os.RequiresNudge() + if tc.parseError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.requiresNudge, req) + } +} diff --git a/server/fleet/service.go b/server/fleet/service.go index 76c29e59d8..d6aa85f0ec 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -34,6 +34,7 @@ type EnterpriseOverrides struct { DeleteMDMAppleBootstrapPackage func(ctx context.Context, teamID *uint) error MDMWindowsEnableOSUpdates func(ctx context.Context, teamID *uint, updates WindowsUpdates) error MDMWindowsDisableOSUpdates func(ctx context.Context, teamID *uint) error + MDMAppleEditedMacOSUpdates func(ctx context.Context, teamID *uint, updates MacOSUpdates) error } type OsqueryService interface { diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 9adc0c1988..761cb01192 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -82,24 +82,31 @@ func GuessProfileExtension(profile []byte) string { } const ( - // FleetdConfigProfileName is the value for the PayloadDisplayName used by // fleetd to read configuration values from the system. FleetdConfigProfileName = "Fleetd configuration" // FleetdFileVaultProfileName is the value for the PayloadDisplayName used // by Fleet to configure FileVault and FileVault Escrow. - FleetFileVaultProfileName = "Disk encryption" + FleetFileVaultProfileName = "Disk encryption" + + // FleetWindowsOSUpdatesProfileName is the name of the profile used by Fleet + // to configure Windows OS updates. FleetWindowsOSUpdatesProfileName = "Windows OS Updates" + + // FleetMacOSUpdatesProfileName is the name of the DDM profile used by Fleet + // to configure macOS OS updates. + FleetMacOSUpdatesProfileName = "Fleet macOS OS Updates" ) -// FleetReservedProfileNames returns a map of PayloadDisplayName strings -// that are reserved by Fleet. +// FleetReservedProfileNames returns a map of PayloadDisplayName or profile +// name strings that are reserved by Fleet. func FleetReservedProfileNames() map[string]struct{} { return map[string]struct{}{ FleetdConfigProfileName: {}, FleetFileVaultProfileName: {}, FleetWindowsOSUpdatesProfileName: {}, + FleetMacOSUpdatesProfileName: {}, } } @@ -114,3 +121,9 @@ func ListFleetReservedWindowsProfileNames() []string { func ListFleetReservedMacOSProfileNames() []string { return []string{FleetFileVaultProfileName, FleetdConfigProfileName} } + +// ListFleetReservedMacOSDeclarationNames returns a list of declaration names +// that are reserved by Fleet for Apple DDM declarations. +func ListFleetReservedMacOSDeclarationNames() []string { + return []string{FleetMacOSUpdatesProfileName} +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 17f945121e..ca698db3a7 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -399,6 +399,8 @@ type InsertCVEMetaFunc func(ctx context.Context, cveMeta []fleet.CVEMeta) error type ListCVEsFunc func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) +type GetHostOperatingSystemFunc func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) + type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error) type ListOperatingSystemsForPlatformFunc func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) @@ -659,6 +661,8 @@ type DeleteMDMAppleConfigProfileByDeprecatedIDFunc func(ctx context.Context, pro type DeleteMDMAppleConfigProfileFunc func(ctx context.Context, profileUUID string) error +type DeleteMDMAppleDeclarationByNameFunc func(ctx context.Context, teamID *uint, name string) error + type BulkDeleteMDMAppleHostsConfigProfilesFunc func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error type DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc func(ctx context.Context, teamID *uint, profileIdentifier string) error @@ -869,6 +873,8 @@ type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles [ type NewMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) +type SetOrUpdateMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) + type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) type SetHostScriptExecutionResultFunc func(ctx context.Context, result *fleet.HostScriptResultPayload) (*fleet.HostScriptResult, error) @@ -1482,6 +1488,9 @@ type DataStore struct { ListCVEsFunc ListCVEsFunc ListCVEsFuncInvoked bool + GetHostOperatingSystemFunc GetHostOperatingSystemFunc + GetHostOperatingSystemFuncInvoked bool + ListOperatingSystemsFunc ListOperatingSystemsFunc ListOperatingSystemsFuncInvoked bool @@ -1872,6 +1881,9 @@ type DataStore struct { DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFuncInvoked bool + DeleteMDMAppleDeclarationByNameFunc DeleteMDMAppleDeclarationByNameFunc + DeleteMDMAppleDeclarationByNameFuncInvoked bool + BulkDeleteMDMAppleHostsConfigProfilesFunc BulkDeleteMDMAppleHostsConfigProfilesFunc BulkDeleteMDMAppleHostsConfigProfilesFuncInvoked bool @@ -2187,6 +2199,9 @@ type DataStore struct { NewMDMAppleDeclarationFunc NewMDMAppleDeclarationFunc NewMDMAppleDeclarationFuncInvoked bool + SetOrUpdateMDMAppleDeclarationFunc SetOrUpdateMDMAppleDeclarationFunc + SetOrUpdateMDMAppleDeclarationFuncInvoked bool + NewHostScriptExecutionRequestFunc NewHostScriptExecutionRequestFunc NewHostScriptExecutionRequestFuncInvoked bool @@ -3583,6 +3598,13 @@ func (s *DataStore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]fleet return s.ListCVEsFunc(ctx, maxAge) } +func (s *DataStore) GetHostOperatingSystem(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + s.mu.Lock() + s.GetHostOperatingSystemFuncInvoked = true + s.mu.Unlock() + return s.GetHostOperatingSystemFunc(ctx, hostID) +} + func (s *DataStore) ListOperatingSystems(ctx context.Context) ([]fleet.OperatingSystem, error) { s.mu.Lock() s.ListOperatingSystemsFuncInvoked = true @@ -4493,6 +4515,13 @@ func (s *DataStore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID return s.DeleteMDMAppleConfigProfileFunc(ctx, profileUUID) } +func (s *DataStore) DeleteMDMAppleDeclarationByName(ctx context.Context, teamID *uint, name string) error { + s.mu.Lock() + s.DeleteMDMAppleDeclarationByNameFuncInvoked = true + s.mu.Unlock() + return s.DeleteMDMAppleDeclarationByNameFunc(ctx, teamID, name) +} + func (s *DataStore) BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error { s.mu.Lock() s.BulkDeleteMDMAppleHostsConfigProfilesFuncInvoked = true @@ -5228,6 +5257,13 @@ func (s *DataStore) NewMDMAppleDeclaration(ctx context.Context, declaration *fle return s.NewMDMAppleDeclarationFunc(ctx, declaration) } +func (s *DataStore) SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { + s.mu.Lock() + s.SetOrUpdateMDMAppleDeclarationFuncInvoked = true + s.mu.Unlock() + return s.SetOrUpdateMDMAppleDeclarationFunc(ctx, declaration) +} + func (s *DataStore) NewHostScriptExecutionRequest(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) { s.mu.Lock() s.NewHostScriptExecutionRequestFuncInvoked = true diff --git a/server/service/appconfig.go b/server/service/appconfig.go index d54e0882c1..4866475b7a 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -556,6 +556,13 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle // activity if oldAppConfig.MDM.MacOSUpdates.MinimumVersion.Value != appConfig.MDM.MacOSUpdates.MinimumVersion.Value || oldAppConfig.MDM.MacOSUpdates.Deadline.Value != appConfig.MDM.MacOSUpdates.Deadline.Value { + if license.IsPremium() { + // macOS updates are premium feature + if err := svc.EnterpriseOverrides.MDMAppleEditedMacOSUpdates(ctx, nil, appConfig.MDM.MacOSUpdates); err != nil { + return nil, ctxerr.Wrap(ctx, err, "update DDM profile after macOS updates change") + } + } + if err := svc.ds.NewActivity( ctx, authz.UserFromContext(ctx), diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 7bdbdb3483..92ca5b8959 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6846,7 +6846,6 @@ func (s *integrationTestSuite) TestChangeUserEmail() { func (s *integrationTestSuite) TestSearchTargets() { t := s.T() - hosts := s.createHosts(t) var builtinNames []string @@ -6874,7 +6873,7 @@ func (s *integrationTestSuite) TestSearchTargets() { s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{LabelIDs: lblIDs}}, http.StatusOK, &searchResp) require.Equal(t, uint(0), searchResp.TargetsCount) require.Len(t, searchResp.Targets.Hosts, len(hosts)) // no omitted host id - require.Len(t, searchResp.Targets.Labels, 0) // labels have been omitted + require.Len(t, searchResp.Targets.Labels, 0) // All built-in labels have been omitted (pre-selected) require.Len(t, searchResp.Targets.Teams, 0) searchResp = searchTargetsResponse{} @@ -6888,7 +6887,7 @@ func (s *integrationTestSuite) TestSearchTargets() { s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{MatchQuery: "foo.local1"}, http.StatusOK, &searchResp) require.Equal(t, uint(0), searchResp.TargetsCount) require.Len(t, searchResp.Targets.Hosts, 1) - require.Len(t, searchResp.Targets.Labels, 1) + require.Len(t, searchResp.Targets.Labels, 1) // with a match query, only matching label names and "All Hosts" can be returned (here, only all hosts) require.Len(t, searchResp.Targets.Teams, 0) require.Contains(t, searchResp.Targets.Hosts[0].Hostname, "foo.local1") } diff --git a/server/service/integration_ddm_test.go b/server/service/integration_ddm_test.go index b4fc49ab65..869277f246 100644 --- a/server/service/integration_ddm_test.go +++ b/server/service/integration_ddm_test.go @@ -62,7 +62,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() { }}, http.StatusUnprocessableEntity) errMsg = extractServerErrorText(res.Body) - require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. OS updates coming soon!") + require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.") // Types from our list of forbidden types should fail for ft := range fleet.ForbiddenDeclTypes { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index a69771912e..b50bd25c7b 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -2188,6 +2188,34 @@ func (s *integrationEnterpriseTestSuite) TestWindowsUpdatesTeamConfig() { }, http.StatusUnprocessableEntity, &tmResp) } +func (s *integrationEnterpriseTestSuite) assertMacOSUpdatesDeclaration(teamID *uint, expected *fleet.MacOSUpdates) { + t := s.T() + if teamID == nil { + teamID = ptr.Uint(0) + } + + var declUUID string + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + err := sqlx.GetContext(context.Background(), q, &declUUID, + `SELECT declaration_uuid FROM mdm_apple_declarations WHERE team_id = ? AND name = ?`, teamID, mdm.FleetMacOSUpdatesProfileName) + if expected == nil { + require.Error(t, err) + return nil + } + return err + }) + + if expected == nil { + // we already validated that the declaration did not exist + return + } + decl, err := s.ds.GetMDMAppleDeclaration(context.Background(), declUUID) + require.NoError(t, err) + + require.Contains(t, string(decl.RawJSON), fmt.Sprintf(`"TargetOSVersion": "%s"`, expected.MinimumVersion.Value)) + require.Contains(t, string(decl.RawJSON), fmt.Sprintf(`"TargetLocalDateTime": "%sT12:00:00"`, expected.Deadline.Value)) +} + func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() { t := s.T() @@ -2202,32 +2230,41 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() { require.Equal(t, team.Name, tmResp.Team.Name) team.ID = tmResp.Team.ID + // no OS updates settings at the moment + s.assertMacOSUpdatesDeclaration(&team.ID, nil) + // modify the team's config + updates := &fleet.MacOSUpdates{ + MinimumVersion: optjson.SetString("10.15.0"), + Deadline: optjson.SetString("2021-01-01"), + } s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{ "mdm": map[string]any{ - "macos_updates": &fleet.MacOSUpdates{ - MinimumVersion: optjson.SetString("10.15.0"), - Deadline: optjson.SetString("2021-01-01"), - }, + "macos_updates": updates, }, }, http.StatusOK, &tmResp) require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value) require.Equal(t, "2021-01-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value) s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "10.15.0", "deadline": "2021-01-01"}`, team.ID, team.Name), 0) + s.assertMacOSUpdatesDeclaration(&team.ID, updates) + // only update the deadline + updates = &fleet.MacOSUpdates{ + MinimumVersion: optjson.SetString("10.15.0"), + Deadline: optjson.SetString("2025-10-01"), + } s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{ "mdm": map[string]any{ - "macos_updates": &fleet.MacOSUpdates{ - MinimumVersion: optjson.SetString("10.15.0"), - Deadline: optjson.SetString("2025-10-01"), - }, + "macos_updates": updates, }, }, http.StatusOK, &tmResp) require.Equal(t, "10.15.0", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value) require.Equal(t, "2025-10-01", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value) lastActivity := s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "10.15.0", "deadline": "2025-10-01"}`, team.ID, team.Name), 0) + s.assertMacOSUpdatesDeclaration(&team.ID, updates) + // setting the windows updates doesn't alter the macos updates tmResp = teamResponse{} s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{ @@ -2246,6 +2283,8 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() { s.lastActivityOfTypeMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), "", lastActivity) lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedWindowsUpdates{}.ActivityName(), ``, 0) + s.assertMacOSUpdatesDeclaration(&team.ID, updates) + // sending a nil MDM or MacOSUpdate config doesn't modify anything s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{ "mdm": nil, @@ -2260,6 +2299,8 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() { // no new activity is created s.lastActivityMatches("", "", lastActivity) + s.assertMacOSUpdatesDeclaration(&team.ID, updates) + // sending macos settings but no macos_updates does not change the macos updates s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{ "mdm": map[string]any{ @@ -2273,6 +2314,8 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() { // no new activity is created s.lastActivityMatches("", "", lastActivity) + s.assertMacOSUpdatesDeclaration(&team.ID, updates) + // sending empty MacOSUpdate fields empties both fields s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), map[string]any{ "mdm": map[string]any{ @@ -2286,6 +2329,8 @@ func (s *integrationEnterpriseTestSuite) TestMacOSUpdatesTeamConfig() { require.Empty(t, tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value) s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q, "minimum_version": "", "deadline": ""}`, team.ID, team.Name), 0) + s.assertMacOSUpdatesDeclaration(&team.ID, nil) + // error checks: // try to set an invalid deadline @@ -2794,6 +2839,8 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // edited macos min version activity got created s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2022-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0) + s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{ + MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2022-01-01")}) // get the appconfig acResp = appConfigResponse{} @@ -2816,6 +2863,8 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // another edited macos min version activity got created lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2024-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0) + s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{ + MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01")}) // update something unrelated - the transparency url acResp = appConfigResponse{} @@ -2825,6 +2874,8 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // no activity got created s.lastActivityMatches("", ``, lastActivity) + s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{ + MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01")}) // clear the macos requirement acResp = appConfigResponse{} @@ -2841,6 +2892,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // edited macos min version activity got created with empty requirement lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"", "minimum_version":"", "team_id": null, "team_name": null}`, 0) + s.assertMacOSUpdatesDeclaration(nil, nil) // update again with empty macos requirement acResp = appConfigResponse{} @@ -2857,6 +2909,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // no activity got created s.lastActivityMatches("", ``, lastActivity) + s.assertMacOSUpdatesDeclaration(nil, nil) } func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index d1a0e4ddc2..139604ad9b 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -855,7 +855,10 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() { require.Len(t, installs, len(wantGlobalProfiles)) require.ElementsMatch(t, wantGlobalProfiles, installs) require.Len(t, removes, len(wantTeamProfiles)) - s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{Verifying: 1}, nil) // host now verifying global profiles + expectedNoTeamSummary = fleet.MDMProfilesSummary{Verifying: 1} + expectedTeamSummary = fleet.MDMProfilesSummary{} + s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) // host now verifying global profiles + s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary) // can't resend profile from another team res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/resend/%s", host.ID, mcUUID), nil, http.StatusNotFound) @@ -872,6 +875,34 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() { res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/resend/%s", host.ID, "z"+uuid.NewString()), nil, http.StatusNotFound) errMsg = extractServerErrorText(res.Body) require.Contains(t, errMsg, "Invalid profile UUID prefix") + + // set OS updates settings for no-team and team, should not change the + // summaries as this profile is ignored. + s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "macos_updates": { + "deadline": "2023-12-31", + "minimum_version": "13.3.7" + } + } + }`), http.StatusOK) + s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID), fleet.TeamPayload{ + MDM: &fleet.TeamPayloadMDM{ + MacOSUpdates: &fleet.MacOSUpdates{ + Deadline: optjson.SetString("1992-01-01"), + MinimumVersion: optjson.SetString("13.1.1"), + }, + }, + }, http.StatusOK) + s.checkMDMProfilesSummaries(t, nil, expectedNoTeamSummary, &expectedNoTeamSummary) + s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary) + + // it should also not show up in the host's profiles list + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", host.ID), getHostRequest{}, http.StatusOK, &hostResp) + require.NotEmpty(t, hostResp.Host.MDM.Profiles) + resProfiles = *hostResp.Host.MDM.Profiles + // one extra profile for the fleetd config + require.Len(t, resProfiles, len(wantGlobalProfiles)+1) } func (s *integrationMDMTestSuite) TestAppleProfileRetries() { @@ -6443,6 +6474,30 @@ func (s *integrationMDMTestSuite) assertMacOSConfigProfilesByName(teamID *uint, }, "a config profile must %s with name: %s", label, profileName) } +func (s *integrationMDMTestSuite) assertMacOSDeclarationsByName(teamID *uint, declarationName string, exists bool) { + t := s.T() + if teamID == nil { + teamID = ptr.Uint(0) + } + var cfgProfs []*fleet.MDMAppleConfigProfile + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_declarations WHERE team_id = ?`, teamID) + }) + + label := "exist" + if !exists { + label = "not exist" + } + require.Condition(t, func() bool { + for _, p := range cfgProfs { + if p.Name == declarationName { + return exists // success if we want it to exist, failure if we don't + } + } + return !exists + }, "a config profile must %s with name: %s", label, declarationName) +} + func (s *integrationMDMTestSuite) assertWindowsConfigProfilesByName(teamID *uint, profileName string, exists bool) { t := s.T() if teamID == nil { @@ -7659,6 +7714,10 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { // nudge config is empty if macos_updates is not set, and Windows MDM notifications are unset h := createOrbitEnrolledHost(t, "darwin", "h", s.ds) + + err := s.ds.UpdateHostOperatingSystem(context.Background(), h.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"}) + require.NoError(t, err) + resp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp) require.Empty(t, resp.NudgeConfig) @@ -7687,7 +7746,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { }) mdmDevice.SerialNumber = h.HardwareSerial mdmDevice.UUID = h.UUID - err := mdmDevice.Enroll() + err = mdmDevice.Enroll() require.NoError(t, err) resp = orbitGetConfigResponse{} @@ -7704,6 +7763,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { Description: "desc team1_" + t.Name(), }) require.NoError(t, err) + s.assertMacOSDeclarationsByName(&team.ID, servermdm.FleetMacOSUpdatesProfileName, false) // add the host to the team err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{h.ID}) @@ -7725,6 +7785,7 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { }, }, }, http.StatusOK, &tmResp) + s.assertMacOSDeclarationsByName(&team.ID, servermdm.FleetMacOSUpdatesProfileName, true) resp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp) @@ -7744,12 +7805,54 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { mdmDevice.UUID = h2.UUID err = mdmDevice.Enroll() require.NoError(t, err) + + err = s.ds.UpdateHostOperatingSystem(context.Background(), h2.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"}) + require.NoError(t, err) + resp = orbitGetConfigResponse{} s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h2.OrbitNodeKey)), http.StatusOK, &resp) wantCfg, err = fleet.NewNudgeConfig(fleet.MacOSUpdates{Deadline: optjson.SetString("2022-01-04"), MinimumVersion: optjson.SetString("12.1.3")}) require.NoError(t, err) require.Equal(t, wantCfg, resp.NudgeConfig) require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC") + + // host on macos > 14, shouldn't be receiving nudge configs + h3 := createOrbitEnrolledHost(t, "darwin", "h3", s.ds) + + mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + }) + mdmDevice.SerialNumber = h3.HardwareSerial + mdmDevice.UUID = h3.UUID + err = mdmDevice.Enroll() + require.NoError(t, err) + + err = s.ds.UpdateHostOperatingSystem(context.Background(), h3.ID, fleet.OperatingSystem{Platform: "darwin", Version: "14.1"}) + require.NoError(t, err) + + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h3.OrbitNodeKey)), http.StatusOK, &resp) + require.Nil(t, resp.NudgeConfig) + + // host is available for nudge, but has not had details query run + // yet, so we don't know the os version. + h4 := createOrbitEnrolledHost(t, "darwin", "h4", s.ds) + + mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + }) + mdmDevice.SerialNumber = h4.HardwareSerial + mdmDevice.UUID = h4.UUID + err = mdmDevice.Enroll() + require.NoError(t, err) + + resp = orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h4.OrbitNodeKey)), http.StatusOK, &resp) + require.Nil(t, resp.NudgeConfig) } func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() { @@ -9220,7 +9323,7 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() { mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { stmt := ` SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, COALESCE(label_id, 0) as label_id - FROM mdm_configuration_profile_labels + FROM mdm_configuration_profile_labels UNION SELECT apple_declaration_uuid as profile_uuid, label_name, COALESCE(label_id, 0) as label_id FROM mdm_declaration_labels ORDER BY profile_uuid, label_name;` return sqlx.SelectContext(context.Background(), q, &profileLabels, stmt) @@ -9393,6 +9496,22 @@ func (s *integrationMDMTestSuite) TestListMDMConfigProfiles() { tm3, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team3"}) require.NoError(t, err) + // set OS Updates settings for team 1 for both macOS and Windows, should not + // be returned by the list profiles endpoint. + var tmResp teamResponse + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1.ID), fleet.TeamPayload{ + MDM: &fleet.TeamPayloadMDM{ + MacOSUpdates: &fleet.MacOSUpdates{ + Deadline: optjson.SetString("1992-01-01"), + MinimumVersion: optjson.SetString("13.1.1"), + }, + WindowsUpdates: &fleet.WindowsUpdates{ + DeadlineDays: optjson.SetInt(5), + GracePeriodDays: optjson.SetInt(2), + }, + }, + }, http.StatusOK, &tmResp) + // create 5 profiles for no team and team 1, names are A, B, C ... for global and // tA, tB, tC ... for team 1. Alternate macOS and Windows profiles. for i := 0; i < 5; i++ { @@ -11250,7 +11369,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() { {Name: "N4", Contents: declarationForTestWithType("D1", "com.apple.configuration.softwareupdate.enforcement.specific")}, }}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID))) errMsg := extractServerErrorText(res.Body) - require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. OS updates coming soon!") + require.Contains(t, errMsg, "Declaration profile can’t include OS updates settings. To control these settings, go to OS updates.") // invalid JSON res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{ @@ -12508,6 +12627,21 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { } } + checkMacDecls := func(teamID *uint, names ...string) { + var count int + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var tid uint + if teamID != nil { + tid = *teamID + } + return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_apple_declarations WHERE team_id = ?`, tid) + }) + require.Equal(t, len(names), count) + for _, n := range names { + s.assertMacOSDeclarationsByName(teamID, n, true) + } + } + checkWinProfs := func(teamID *uint, names ...string) { var count int mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { @@ -12517,6 +12651,7 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { } return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tid) }) + require.Equal(t, len(names), count) for _, n := range names { s.assertWindowsConfigProfilesByName(teamID, n, true) } @@ -12545,11 +12680,12 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { }, "macos_updates": { "deadline": "2023-12-31", - "minimum_version": "13.3.7" + "minimum_version": "13.3.6" } } }`), http.StatusOK, &acResp) checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...) + checkMacDecls(nil, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...) // batch set only windows profiles doesn't remove the reserved names @@ -12561,6 +12697,7 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { }) s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...) + checkMacDecls(nil, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) // batch set windows and mac profiles doesn't remove the reserved names @@ -12571,15 +12708,27 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { }) s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkMacDecls(nil, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) - // batch set only mac profiles doesn't remove the reserved names + // batch set only mac profiles and declaration doesn't remove the reserved names + newMacDecl := []byte(fmt.Sprintf(`{ + "Type": "com.apple.configuration.foo", + "Payload": { + "Echo": "f1337" + }, + "Identifier": "%s" +}`, uuid.NewString())) testProfiles = []fleet.MDMProfileBatchPayload{{ Name: "n2", Contents: newMacProfile, + }, { + Name: "n3", + Contents: newMacDecl, }} s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent) checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkMacDecls(nil, append(servermdm.ListFleetReservedMacOSDeclarationNames(), "n3")...) checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...) // create a team @@ -12598,7 +12747,7 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { }, MacOSUpdates: &fleet.MacOSUpdates{ Deadline: optjson.SetString("2023-12-31"), - MinimumVersion: optjson.SetString("13.3.8"), + MinimumVersion: optjson.SetString("13.3.9"), }, }, }, @@ -12609,11 +12758,12 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { require.Equal(t, 4, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value) require.Equal(t, 1, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value) require.Equal(t, "2023-12-31", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value) - require.Equal(t, "13.3.8", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value) + require.Equal(t, "13.3.9", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value) require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)) checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkMacDecls(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) // batch set only windows profiles doesn't remove the reserved names @@ -12624,6 +12774,7 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { }) s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkMacDecls(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) // batch set windows and mac profiles doesn't remove the reserved names @@ -12633,14 +12784,25 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() { }) s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkMacDecls(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...) - // batch set only mac profiles doesn't remove the reserved names + // batch set only mac profiles and declaration doesn't remove the reserved names testTeamProfiles = []fleet.MDMProfileBatchPayload{{ Name: "n2", Contents: newMacProfile, + }, { + Name: "n3", + Contents: newMacDecl, }} s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...) + checkMacDecls(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSDeclarationNames(), "n3")...) + checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) + + // batch set with an empty set still doesn't remove the Fleet-controlled profiles + s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID))) + checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...) + checkMacDecls(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSDeclarationNames()...) checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...) } diff --git a/server/service/orbit.go b/server/service/orbit.go index 63809ba902..05230cabeb 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -2,6 +2,7 @@ package service import ( "context" + "database/sql" "encoding/json" "errors" "fmt" @@ -268,10 +269,24 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro if appConfig.MDM.EnabledAndConfigured && mdmConfig != nil && mdmConfig.MacOSUpdates.EnabledForHost(host) { - nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates) + hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID) + if errors.Is(err, sql.ErrNoRows) { + // host os has not been collected yet (no details query) + hostOS = &fleet.OperatingSystem{} + } else if err != nil { + return fleet.OrbitConfig{}, err + } + requiresNudge, err := hostOS.RequiresNudge() if err != nil { return fleet.OrbitConfig{}, err } + + if requiresNudge { + nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates) + if err != nil { + return fleet.OrbitConfig{}, err + } + } } if mdmConfig.EnableDiskEncryption && @@ -313,10 +328,25 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro var nudgeConfig *fleet.NudgeConfig if appConfig.MDM.EnabledAndConfigured && appConfig.MDM.MacOSUpdates.EnabledForHost(host) { - nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates) + hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID) + if errors.Is(err, sql.ErrNoRows) { + // host os has not been collected yet (no details query) + hostOS = &fleet.OperatingSystem{} + } else if err != nil { + return fleet.OrbitConfig{}, err + } + requiresNudge, err := hostOS.RequiresNudge() if err != nil { return fleet.OrbitConfig{}, err } + + if requiresNudge { + nudgeConfig, err = fleet.NewNudgeConfig(appConfig.MDM.MacOSUpdates) + if err != nil { + return fleet.OrbitConfig{}, err + + } + } } if appConfig.MDM.WindowsEnabledAndConfigured && diff --git a/server/service/orbit_test.go b/server/service/orbit_test.go index b294983de1..97a203d86b 100644 --- a/server/service/orbit_test.go +++ b/server/service/orbit_test.go @@ -22,11 +22,19 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return appCfg, nil } + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { return nil, nil } ctx = test.HostContext(ctx, &fleet.Host{ OsqueryHostID: ptr.String("test"), + ID: 1, MDMInfo: &fleet.HostMDM{ IsServer: false, InstalledFromDep: true, @@ -65,7 +73,13 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return appCfg, nil } - + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } team := fleet.Team{ID: 1} teamMDM := fleet.TeamMDM{} ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { @@ -81,6 +95,7 @@ func TestGetOrbitConfigNudge(t *testing.T) { ctx = test.HostContext(ctx, &fleet.Host{ OsqueryHostID: ptr.String("test"), + ID: 1, TeamID: ptr.Uint(team.ID), MDMInfo: &fleet.HostMDM{ IsServer: false, @@ -120,6 +135,13 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds := new(mock.Store) license := &fleet.LicenseInfo{Tier: fleet.TierPremium} svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}} appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("2022-04-01") @@ -193,4 +215,103 @@ func TestGetOrbitConfigNudge(t *testing.T) { }}) }) + + t.Run("no-nudge on macos versions greater than 14", func(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + os := &fleet.OperatingSystem{ + Platform: "darwin", + Version: "12.2", + } + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + MDMInfo: &fleet.HostMDM{ + IsServer: false, + InstalledFromDep: true, + Enrolled: true, + Name: fleet.WellKnownMDMFleet, + }} + + team := fleet.Team{ID: 1} + teamMDM := fleet.TeamMDM{} + teamMDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") + teamMDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.1") + ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { + require.Equal(t, team.ID, teamID) + return &teamMDM, nil + } + ds.TeamAgentOptionsFunc = func(ctx context.Context, id uint) (*json.RawMessage, error) { + return ptr.RawMessage(json.RawMessage(`{}`)), nil + } + ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { + return nil, nil + } + + appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}} + appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") + appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.3") + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appCfg, nil + } + ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { + return nil, nil + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } + ctx = test.HostContext(ctx, host) + + // Version < 14 gets nudge + host.ID = 1 + cfg, err := svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.NotEmpty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // Version > 14 gets no nudge + os.Version = "14.1" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // windows gets no nudge + os.Platform = "windows" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + //// team section below + host.TeamID = ptr.Uint(team.ID) + os.Platform = "darwin" + os.Version = "12.1" + + // Version < 14 gets nudge + host.ID = 1 + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.NotEmpty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // Version > 14 gets no nudge + os.Version = "14.1" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + + // windows gets no nudge + os.Platform = "windows" + ds.GetHostOperatingSystemFuncInvoked = false + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.Empty(t, cfg.NudgeConfig) + require.True(t, ds.GetHostOperatingSystemFuncInvoked) + }) } diff --git a/server/service/testing_client.go b/server/service/testing_client.go index 493d35bccb..7a182f5652 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -40,6 +40,11 @@ type withDS struct { func (ts *withDS) SetupSuite(dbName string) { t := ts.s.T() ts.ds = mysql.CreateNamedMySQLDS(t, dbName) + // remove any migration-created labels + mysql.ExecAdhocSQL(t, ts.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), `DELETE FROM labels`) + return err + }) test.AddBuiltinLabels(t, ts.ds) // setup the required fields on AppConfig diff --git a/server/test/new_objects.go b/server/test/new_objects.go index 6bf4953320..cf92fb798d 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -156,6 +156,12 @@ func AddBuiltinLabels(t *testing.T, ds fleet.Datastore) { LabelType: fleet.LabelTypeBuiltIn, LabelMembershipType: fleet.LabelMembershipTypeDynamic, }, + { + Name: fleet.BuiltinLabelMacOS14Plus, + Query: "select 1 from os_version where platform = 'darwin' and major >= 14;", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeDynamic, + }, } names := fleet.ReservedLabelNames()