mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Add feature to manage macOS software updates via DDM (#18281)
Feature branch for #17295
This commit is contained in:
commit
999e200992
44 changed files with 1122 additions and 131 deletions
BIN
assets/images/macos-updates-preview.png
Normal file
BIN
assets/images/macos-updates-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
1
changes/17418-macos-14-nudge
Normal file
1
changes/17418-macos-14-nudge
Normal file
|
|
@ -0,0 +1 @@
|
|||
* macOS 14 and higher no longer display nudge notifications
|
||||
1
changes/17420-update-ddm-profile-os-updates
Normal file
1
changes/17420-update-ddm-profile-os-updates
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added creation or update of macOS DDM profile to enforce OS Updates settings whenever the settings are changed.
|
||||
1
changes/issue-17417-ui-os-updates-ddm
Normal file
1
changes/issue-17417-ui-os-updates-ddm
Normal file
|
|
@ -0,0 +1 @@
|
|||
- change UI on OS Updates page to show new nudge for macos DDM
|
||||
|
|
@ -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, `---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ func NewService(
|
|||
DeleteMDMAppleBootstrapPackage: eeservice.DeleteMDMAppleBootstrapPackage,
|
||||
MDMWindowsEnableOSUpdates: eeservice.mdmWindowsEnableOSUpdates,
|
||||
MDMWindowsDisableOSUpdates: eeservice.mdmWindowsDisableOSUpdates,
|
||||
MDMAppleEditedMacOSUpdates: eeservice.mdmAppleEditedMacOSUpdates,
|
||||
})
|
||||
|
||||
return eeservice, nil
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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.
|
||||
</p>
|
||||
<CustomLink
|
||||
text="Learn more about Windows updates in Fleet"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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+)")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue