Add feature to manage macOS software updates via DDM (#18281)

Feature branch for #17295
This commit is contained in:
George Karr 2024-04-16 15:18:40 -05:00 committed by GitHub
commit 999e200992
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1122 additions and 131 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View file

@ -0,0 +1 @@
* macOS 14 and higher no longer display nudge notifications

View file

@ -0,0 +1 @@
* Added creation or update of macOS DDM profile to enforce OS Updates settings whenever the settings are changed.

View file

@ -0,0 +1 @@
- change UI on OS Updates page to show new nudge for macos DDM

View file

@ -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, `---

View file

@ -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)

View file

@ -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{

View file

@ -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{

View file

@ -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{}

View file

@ -76,6 +76,7 @@ func NewService(
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
MDMWindowsEnableOSUpdates: eeservice.mdmWindowsEnableOSUpdates,
MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates,
MDMAppleEditedMacOSUpdates: eeservice.mdmAppleEditedMacOSUpdates,
})
return eeservice, nil

View file

@ -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
}

View file

@ -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<OSUpdatesSupportedPlatform | null>(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<IConfig, Error>(["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 <Spinner />;
// FIXME: Handle error states for app config and team config (need specifications for this).
@ -118,11 +112,7 @@ const OSUpdates = ({ router, teamIdForApi }: IOSUpdates) => {
</div>
<div className={`${baseClass}__taget-container`}>
<TargetSection
key={generateKey({
currentTeamId: teamIdForApi,
appConfig: config,
teamConfig,
})} // FIXME: Find a better way to trigger re-rendering if these change (see FIXME above regarding refetching)
key={teamIdForApi} // if the team changes, remount the target section
appConfig={config}
currentTeamId={teamIdForApi}
isFetching={isFetchingConfig || isFetchingTeamConfig}

View file

@ -4,7 +4,7 @@ import CustomLink from "components/CustomLink";
import { OSUpdatesSupportedPlatform } from "../../OSUpdates";
import MacOSUpdateScreenshot from "../../../../../../assets/images/nudge-screenshot.png";
import MacOSUpdateScreenshot from "../../../../../../assets/images/macos-updates-preview.png";
import WindowsUpdateScreenshot from "../../../../../../assets/images/windows-nudge-screenshot.png";
const baseClass = "nudge-preview";
@ -17,12 +17,12 @@ const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {
<>
<h3>End user experience on macOS</h3>
<p>
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).
</p>
<p>As the deadline gets closer, Fleet provides stronger encouragement.</p>
<p>Everyone else will see the Nudge window.</p>
<CustomLink
text="Learn more about macOS updates in Fleet"
text="Learn more"
url="https://fleetdm.com/learn-more-about/os-updates"
newTab
/>
@ -33,8 +33,8 @@ const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {
<p>
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
users local time). After the deadline, restarts are forced regardless
of active hours.
user&apos;s local time). After the deadline, restarts are forced
regardless of active hours.
</p>
<CustomLink
text="Learn more about Windows updates in Fleet"

View file

@ -59,16 +59,6 @@ const getDefaultWindowsGracePeriodDays = ({
: teamConfig?.mdm?.windows_updates.grace_period_days?.toString() ?? "";
};
export const generateKey = (args: GetDefaultFnParams) => {
return (
`${args.currentTeamId}-` +
`${getDefaultMacOSDeadline(args)}-` +
`${getDefaultMacOSVersion(args)}-` +
`${getDefaultWindowsDeadlineDays(args)}-` +
`${getDefaultWindowsGracePeriodDays(args)}`
);
};
interface ITargetSectionProps {
appConfig: IConfig;
currentTeamId: number;

View file

@ -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
}

View file

@ -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()

View file

@ -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
}

View file

@ -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")

View file

@ -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
}

View file

@ -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+)")
}

View file

@ -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
}

View file

@ -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
}

File diff suppressed because one or more lines are too long

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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 cant include OS updates settings. OS updates coming soon!")
return NewInvalidArgumentError(r.Type, "Declaration profile cant include OS updates settings. To control these settings, go to OS updates.")
}
if _, forbidden := ForbiddenDeclTypes[r.Type]; forbidden {

View file

@ -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

View file

@ -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: {},
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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}
}

View file

@ -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

View file

@ -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),

View file

@ -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")
}

View file

@ -62,7 +62,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
}}, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Declaration profile cant include OS updates settings. OS updates coming soon!")
require.Contains(t, errMsg, "Declaration profile cant 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 {

View file

@ -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() {

View file

@ -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 cant include OS updates settings. OS updates coming soon!")
require.Contains(t, errMsg, "Declaration profile cant 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()...)
}

View file

@ -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 &&

View file

@ -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)
})
}

View file

@ -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

View file

@ -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()