diff --git a/changes/15461-host-dep-assign-profile-responses b/changes/15461-host-dep-assign-profile-responses new file mode 100644 index 0000000000..4bb2178425 --- /dev/null +++ b/changes/15461-host-dep-assign-profile-responses @@ -0,0 +1 @@ +- Added functionality to surface MDM devices where DEP assignment failed. \ No newline at end of file diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 78873db9b6..fad23d1d15 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -643,6 +643,9 @@ func newWorkerIntegrationsSchedule( } return nil }), + schedule.WithJob("dep_cooldowns", func(ctx context.Context) error { + return worker.ProcessDEPCooldowns(ctx, ds, logger) + }), ) return s, nil diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json index 7772e27166..8c6e8dc3a7 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json @@ -1,100 +1,101 @@ { - "kind": "host", - "apiVersion": "v1", - "spec": { - "created_at": "0001-01-01T00:00:00Z", - "updated_at": "0001-01-01T00:00:00Z", - "id": 0, - "detail_updated_at": "0001-01-01T00:00:00Z", - "label_updated_at": "0001-01-01T00:00:00Z", - "policy_updated_at": "0001-01-01T00:00:00Z", - "last_enrolled_at": "0001-01-01T00:00:00Z", - "last_restarted_at": "0001-01-01T00:00:00Z", - "seen_time": "0001-01-01T00:00:00Z", - "software_updated_at": "0001-01-01T00:00:00Z", - "refetch_requested": false, - "refetch_critical_queries_until": null, - "hostname": "test_host", - "uuid": "", - "platform": "", - "osquery_version": "", - "os_version": "", - "build": "", - "platform_like": "", - "code_name": "", - "uptime": 0, - "memory": 0, - "cpu_type": "", - "cpu_subtype": "", - "cpu_brand": "", - "cpu_physical_cores": 0, - "cpu_logical_cores": 0, - "hardware_vendor": "", - "hardware_model": "", - "hardware_version": "", - "hardware_serial": "", - "computer_name": "test_host", - "public_ip": "", - "primary_ip": "", - "primary_mac": "", - "distributed_interval": 0, - "config_tls_refresh": 0, - "logger_tls_period": 0, - "mdm": { - "device_status": "unlocked", - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "pending_action": "", - "server_url": null - }, - "team_id": null, - "pack_stats": null, - "team_name": null, - "gigs_disk_space_available": 0, - "percent_disk_space_available": 0, - "gigs_total_disk_space": 0, - "issues": { - "total_issues_count": 0, - "failing_policies_count": 0 - }, - "labels": [], - "packs": [], - "policies": [ - { - "id": 1, - "query": "select 1 from osquery_info where start_time > 1;", - "name": "query1", - "platform": "", - "description": "Some description", - "author_email": "alice@example.com", - "author_id": 1, - "author_name": "Alice", - "response": "passes", - "resolution": "Some resolution", - "team_id": 1, - "updated_at": "0001-01-01T00:00:00Z", - "created_at": "0001-01-01T00:00:00Z", - "critical": false - }, - { - "id": 2, - "query": "select 1 from osquery_info where start_time > 1;", - "name": "query2", - "platform": "", - "description": "", - "author_email": "alice@example.com", - "author_id": 1, - "author_name": "Alice", - "response": "fails", - "team_id": null, - "updated_at": "0001-01-01T00:00:00Z", - "created_at": "0001-01-01T00:00:00Z", - "critical": false - } - ], - "status": "offline", - "display_text": "test_host", - "display_name": "test_host" - } + "kind": "host", + "apiVersion": "v1", + "spec": { + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "id": 0, + "detail_updated_at": "0001-01-01T00:00:00Z", + "label_updated_at": "0001-01-01T00:00:00Z", + "policy_updated_at": "0001-01-01T00:00:00Z", + "last_enrolled_at": "0001-01-01T00:00:00Z", + "last_restarted_at": "0001-01-01T00:00:00Z", + "seen_time": "0001-01-01T00:00:00Z", + "software_updated_at": "0001-01-01T00:00:00Z", + "refetch_requested": false, + "refetch_critical_queries_until": null, + "hostname": "test_host", + "uuid": "", + "platform": "", + "osquery_version": "", + "os_version": "", + "build": "", + "platform_like": "", + "code_name": "", + "uptime": 0, + "memory": 0, + "cpu_type": "", + "cpu_subtype": "", + "cpu_brand": "", + "cpu_physical_cores": 0, + "cpu_logical_cores": 0, + "hardware_vendor": "", + "hardware_model": "", + "hardware_version": "", + "hardware_serial": "", + "computer_name": "test_host", + "public_ip": "", + "primary_ip": "", + "primary_mac": "", + "distributed_interval": 0, + "config_tls_refresh": 0, + "logger_tls_period": 0, + "mdm": { + "dep_profile_error": false, + "device_status": "unlocked", + "encryption_key_available": false, + "enrollment_status": null, + "name": "", + "pending_action": "", + "server_url": null + }, + "team_id": null, + "pack_stats": null, + "team_name": null, + "gigs_disk_space_available": 0, + "percent_disk_space_available": 0, + "gigs_total_disk_space": 0, + "issues": { + "total_issues_count": 0, + "failing_policies_count": 0 + }, + "labels": [], + "packs": [], + "policies": [ + { + "id": 1, + "query": "select 1 from osquery_info where start_time > 1;", + "name": "query1", + "platform": "", + "description": "Some description", + "author_email": "alice@example.com", + "author_id": 1, + "author_name": "Alice", + "response": "passes", + "resolution": "Some resolution", + "team_id": 1, + "updated_at": "0001-01-01T00:00:00Z", + "created_at": "0001-01-01T00:00:00Z", + "critical": false + }, + { + "id": 2, + "query": "select 1 from osquery_info where start_time > 1;", + "name": "query2", + "platform": "", + "description": "", + "author_email": "alice@example.com", + "author_id": 1, + "author_name": "Alice", + "response": "fails", + "team_id": null, + "updated_at": "0001-01-01T00:00:00Z", + "created_at": "0001-01-01T00:00:00Z", + "critical": false + } + ], + "status": "offline", + "display_text": "test_host", + "display_name": "test_host" + } } diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml index dfb7b26323..fc4431c9e1 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml @@ -32,6 +32,7 @@ spec: last_restarted_at: "0001-01-01T00:00:00Z" logger_tls_period: 0 mdm: + dep_profile_error: false device_status: unlocked encryption_key_available: false enrollment_status: null diff --git a/cmd/fleetctl/testdata/expectedListHostsJson.json b/cmd/fleetctl/testdata/expectedListHostsJson.json index a97e058b3e..2d80597d4b 100644 --- a/cmd/fleetctl/testdata/expectedListHostsJson.json +++ b/cmd/fleetctl/testdata/expectedListHostsJson.json @@ -42,6 +42,7 @@ "config_tls_refresh": 0, "logger_tls_period": 0, "mdm": { + "dep_profile_error": false, "encryption_key_available": false, "enrollment_status": null, "name": "", @@ -113,6 +114,7 @@ "config_tls_refresh": 0, "logger_tls_period": 0, "mdm": { + "dep_profile_error": false, "encryption_key_available": false, "enrollment_status": null, "name": "", diff --git a/cmd/fleetctl/testdata/expectedListHostsMDM.json b/cmd/fleetctl/testdata/expectedListHostsMDM.json index 1c83f5ff27..fcd180320c 100644 --- a/cmd/fleetctl/testdata/expectedListHostsMDM.json +++ b/cmd/fleetctl/testdata/expectedListHostsMDM.json @@ -1,136 +1,138 @@ [ - { - "kind": "host", - "apiVersion": "v1", - "spec": { - "created_at": "0001-01-01T00:00:00Z", - "updated_at": "0001-01-01T00:00:00Z", - "id": 0, - "detail_updated_at": "0001-01-01T00:00:00Z", - "label_updated_at": "0001-01-01T00:00:00Z", - "last_enrolled_at": "0001-01-01T00:00:00Z", - "last_restarted_at": "0001-01-01T00:00:00Z", - "seen_time": "0001-01-01T00:00:00Z", - "software_updated_at": "0001-01-01T00:00:00Z", - "refetch_requested": false, - "refetch_critical_queries_until": null, - "hostname": "test_host", - "display_name": "test_host", - "uuid": "", - "platform": "", - "osquery_version": "", - "os_version": "", - "build": "", - "platform_like": "", - "policy_updated_at": "0001-01-01T00:00:00Z", - "code_name": "", - "uptime": 0, - "memory": 0, - "cpu_type": "", - "cpu_subtype": "", - "cpu_brand": "", - "cpu_physical_cores": 0, - "cpu_logical_cores": 0, - "hardware_vendor": "", - "hardware_model": "", - "hardware_version": "", - "hardware_serial": "", - "computer_name": "test_host", - "public_ip": "", - "primary_ip": "", - "primary_mac": "", - "distributed_interval": 0, - "config_tls_refresh": 0, - "logger_tls_period": 0, - "mdm": { - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "server_url": null - }, - "team_id": null, - "pack_stats": null, - "team_name": null, - "additional": { - "query1": [ - { - "col1": "val", - "col2": 42 - } - ] - }, - "gigs_disk_space_available": 0, - "percent_disk_space_available": 0, - "gigs_total_disk_space": 0, - "issues": { - "total_issues_count": 0, - "failing_policies_count": 0 - }, - "status": "offline", - "display_text": "test_host" - } - }, - { - "kind": "host", - "apiVersion": "v1", - "spec": { - "created_at": "0001-01-01T00:00:00Z", - "updated_at": "0001-01-01T00:00:00Z", - "id": 0, - "detail_updated_at": "0001-01-01T00:00:00Z", - "label_updated_at": "0001-01-01T00:00:00Z", - "last_enrolled_at": "0001-01-01T00:00:00Z", - "last_restarted_at": "0001-01-01T00:00:00Z", - "seen_time": "0001-01-01T00:00:00Z", - "software_updated_at": "0001-01-01T00:00:00Z", - "refetch_requested": false, - "refetch_critical_queries_until": null, - "hostname": "test_host2", - "uuid": "", - "platform": "", - "osquery_version": "", - "os_version": "", - "build": "", - "platform_like": "", - "policy_updated_at": "0001-01-01T00:00:00Z", - "code_name": "", - "uptime": 0, - "memory": 0, - "cpu_type": "", - "cpu_subtype": "", - "cpu_brand": "", - "cpu_physical_cores": 0, - "cpu_logical_cores": 0, - "hardware_vendor": "", - "hardware_model": "", - "hardware_version": "", - "hardware_serial": "", - "computer_name": "test_host2", - "display_name": "test_host2", - "public_ip": "", - "primary_ip": "", - "primary_mac": "", - "distributed_interval": 0, - "config_tls_refresh": 0, - "logger_tls_period": 0, - "mdm": { - "encryption_key_available": false, - "enrollment_status": null, - "name": "", - "server_url": null - }, - "team_id": null, - "pack_stats": null, - "team_name": null, - "gigs_disk_space_available": 0, - "percent_disk_space_available": 0, - "gigs_total_disk_space": 0, - "issues": { - "total_issues_count": 0, - "failing_policies_count": 0 - }, - "status": "offline", - "display_text": "test_host2" - } - } + { + "kind": "host", + "apiVersion": "v1", + "spec": { + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "id": 0, + "detail_updated_at": "0001-01-01T00:00:00Z", + "label_updated_at": "0001-01-01T00:00:00Z", + "last_enrolled_at": "0001-01-01T00:00:00Z", + "last_restarted_at": "0001-01-01T00:00:00Z", + "seen_time": "0001-01-01T00:00:00Z", + "software_updated_at": "0001-01-01T00:00:00Z", + "refetch_requested": false, + "refetch_critical_queries_until": null, + "hostname": "test_host", + "display_name": "test_host", + "uuid": "", + "platform": "", + "osquery_version": "", + "os_version": "", + "build": "", + "platform_like": "", + "policy_updated_at": "0001-01-01T00:00:00Z", + "code_name": "", + "uptime": 0, + "memory": 0, + "cpu_type": "", + "cpu_subtype": "", + "cpu_brand": "", + "cpu_physical_cores": 0, + "cpu_logical_cores": 0, + "hardware_vendor": "", + "hardware_model": "", + "hardware_version": "", + "hardware_serial": "", + "computer_name": "test_host", + "public_ip": "", + "primary_ip": "", + "primary_mac": "", + "distributed_interval": 0, + "config_tls_refresh": 0, + "logger_tls_period": 0, + "mdm": { + "dep_profile_error": false, + "encryption_key_available": false, + "enrollment_status": null, + "name": "", + "server_url": null + }, + "team_id": null, + "pack_stats": null, + "team_name": null, + "additional": { + "query1": [ + { + "col1": "val", + "col2": 42 + } + ] + }, + "gigs_disk_space_available": 0, + "percent_disk_space_available": 0, + "gigs_total_disk_space": 0, + "issues": { + "total_issues_count": 0, + "failing_policies_count": 0 + }, + "status": "offline", + "display_text": "test_host" + } + }, + { + "kind": "host", + "apiVersion": "v1", + "spec": { + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "id": 0, + "detail_updated_at": "0001-01-01T00:00:00Z", + "label_updated_at": "0001-01-01T00:00:00Z", + "last_enrolled_at": "0001-01-01T00:00:00Z", + "last_restarted_at": "0001-01-01T00:00:00Z", + "seen_time": "0001-01-01T00:00:00Z", + "software_updated_at": "0001-01-01T00:00:00Z", + "refetch_requested": false, + "refetch_critical_queries_until": null, + "hostname": "test_host2", + "uuid": "", + "platform": "", + "osquery_version": "", + "os_version": "", + "build": "", + "platform_like": "", + "policy_updated_at": "0001-01-01T00:00:00Z", + "code_name": "", + "uptime": 0, + "memory": 0, + "cpu_type": "", + "cpu_subtype": "", + "cpu_brand": "", + "cpu_physical_cores": 0, + "cpu_logical_cores": 0, + "hardware_vendor": "", + "hardware_model": "", + "hardware_version": "", + "hardware_serial": "", + "computer_name": "test_host2", + "display_name": "test_host2", + "public_ip": "", + "primary_ip": "", + "primary_mac": "", + "distributed_interval": 0, + "config_tls_refresh": 0, + "logger_tls_period": 0, + "mdm": { + "dep_profile_error": false, + "encryption_key_available": false, + "enrollment_status": null, + "name": "", + "server_url": null + }, + "team_id": null, + "pack_stats": null, + "team_name": null, + "gigs_disk_space_available": 0, + "percent_disk_space_available": 0, + "gigs_total_disk_space": 0, + "issues": { + "total_issues_count": 0, + "failing_policies_count": 0 + }, + "status": "offline", + "display_text": "test_host2" + } + } ] diff --git a/cmd/fleetctl/testdata/expectedListHostsYaml.yml b/cmd/fleetctl/testdata/expectedListHostsYaml.yml index 249b4b884e..ac733c2b83 100644 --- a/cmd/fleetctl/testdata/expectedListHostsYaml.yml +++ b/cmd/fleetctl/testdata/expectedListHostsYaml.yml @@ -36,6 +36,7 @@ spec: last_restarted_at: "0001-01-01T00:00:00Z" logger_tls_period: 0 mdm: + dep_profile_error: false encryption_key_available: false enrollment_status: null name: "" @@ -93,6 +94,7 @@ spec: last_restarted_at: "0001-01-01T00:00:00Z" logger_tls_period: 0 mdm: + dep_profile_error: false encryption_key_available: false enrollment_status: null server_url: null diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 0c28d4a2a2..5d145bbf21 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -258,7 +258,7 @@ func (svc *Service) updateAppConfigMDMAppleSetup(ctx context.Context, payload fl } func (svc *Service) updateMacOSSetupEnableEndUserAuth(ctx context.Context, enable bool, teamID *uint, teamName *string) error { - if err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantUpdateProfile, teamID); err != nil { + if _, err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantUpdateProfile, teamID); err != nil { return ctxerr.Wrap(ctx, err, "queue macos setup assistant update profile job") } @@ -574,7 +574,7 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst // if the name is the same and the content did not change, uploaded at will stay the same if prevAsst == nil || newAsst.Name != prevAsst.Name || newAsst.UploadedAt.After(prevAsst.UploadedAt) { - if err := worker.QueueMacosSetupAssistantJob( + if _, err := worker.QueueMacosSetupAssistantJob( ctx, svc.ds, svc.logger, @@ -618,7 +618,7 @@ func (svc *Service) DeleteMDMAppleSetupAssistant(ctx context.Context, teamID *ui } if prevAsst != nil { - if err := worker.QueueMacosSetupAssistantJob( + if _, err := worker.QueueMacosSetupAssistantJob( ctx, svc.ds, svc.logger, @@ -802,7 +802,7 @@ func (svc *Service) mdmSSOHandleCallbackAuth(ctx context.Context, auth fleet.Aut } func (svc *Service) mdmAppleSyncDEPProfiles(ctx context.Context) error { - if err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantUpdateAllProfiles, nil); err != nil { + if _, err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantUpdateAllProfiles, nil); err != nil { return ctxerr.Wrap(ctx, err, "queue macos setup assistant update all profiles job") } return nil diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index ffb5a908eb..a194e78b6d 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -519,6 +519,9 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error { mdmHostSerials := make([]string, 0, len(hosts)) for _, host := range hosts { hostIDs = append(hostIDs, host.ID) + // FIXME: These checks don't work here because host.MDMInfo is not being populated by + // ds.ListHosts call (it populates host.MDM instead). This may be happening in other + // places too. if host.MDMInfo.IsPendingDEPFleetEnrollment() || host.MDMInfo.IsDEPFleetEnrolled() { mdmHostSerials = append(mdmHostSerials, host.HardwareSerial) } @@ -538,7 +541,7 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error { } if len(mdmHostSerials) > 0 { - if err := worker.QueueMacosSetupAssistantJob( + if _, err := worker.QueueMacosSetupAssistantJob( ctx, svc.ds, svc.logger, diff --git a/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tsx b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tsx new file mode 100644 index 0000000000..36917063a2 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import ReactTooltip from "react-tooltip"; +import { COLORS } from "styles/var/colors"; +import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; +import Icon from "components/Icon"; +import NotSupported from "components/NotSupported"; +import { IHost } from "interfaces/host"; + +const baseClass = "host-mdm-status-cell"; + +const HostMdmStatusCell = ({ + row: { + original: { id, mdm, platform }, + }, + cell: { value }, +}: { + row: { original: IHost }; + cell: { value: string }; +}): JSX.Element => { + if (platform === "chrome") { + return NotSupported; + } + + if (!value) { + return {DEFAULT_EMPTY_CELL_VALUE}; + } + + return ( + + {value} + {mdm?.dep_profile_error && ( + <> + + + + + + Fleet hit Apple’s API rate limit when preparing the macOS Setup + Assistant for this host. Fleet will try again every hour. + + + + )} + + ); +}; + +export default HostMdmStatusCell; diff --git a/frontend/components/TableContainer/DataTable/HostMdmStatusCell/_styles.scss b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/_styles.scss new file mode 100644 index 0000000000..fdb976ae47 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/_styles.scss @@ -0,0 +1,10 @@ +.host-mdm-status-cell { + display: inline-flex; + flex-direction: row; + text-wrap: nowrap; + gap: $pad-small; + + .tooltip__tooltip-text { + text-wrap: wrap; + } +} diff --git a/frontend/components/TableContainer/DataTable/HostMdmStatusCell/index.ts b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/index.ts new file mode 100644 index 0000000000..c5943e3fa4 --- /dev/null +++ b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/index.ts @@ -0,0 +1 @@ +export { default } from "./HostMdmStatusCell"; diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 8df2467664..420591fa39 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -163,6 +163,7 @@ export type HostMdmPendingAction = "unlock" | "lock" | ""; export interface IHostMdmData { encryption_key_available: boolean; enrollment_status: MdmEnrollmentStatus | null; + dep_profile_error?: boolean; name?: string; id?: number; server_url: string | null; diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 63bec0daf9..f3f0ff9b7f 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -9,6 +9,7 @@ import { IDeviceUser, IHost } from "interfaces/host"; import Checkbox from "components/forms/fields/Checkbox"; import DiskSpaceGraph from "components/DiskSpaceGraph"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; +import HostMdmStatusCell from "components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell"; import IssueCell from "components/TableContainer/DataTable/IssueCell/IssueCell"; import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell"; import StatusIndicator from "components/StatusIndicator"; @@ -402,15 +403,7 @@ const allHostTableHeaders: IDataColumn[] = [ disableSortBy: true, accessor: "mdm.enrollment_status", id: "mdm_enrollment_status", - Cell: (cellProps: ICellProps) => { - if (cellProps.row.original.platform === "chrome") { - return NotSupported; - } - if (cellProps.cell.value) { - return ; - } - return {DEFAULT_EMPTY_CELL_VALUE}; - }, + Cell: HostMdmStatusCell, }, { title: "MDM server URL", diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index bc56a327be..e93eab08f7 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -2852,6 +2852,189 @@ func (ds *Datastore) GetMDMAppleDefaultSetupAssistant(ctx context.Context, teamI return asst.ProfileUUID, asst.UploadedAt, nil } +func (ds *Datastore) UpdateHostDEPAssignProfileResponses(ctx context.Context, payload *godep.ProfileResponse) error { + if payload == nil { + // caller should ensure this does not happen + level.Debug(ds.logger).Log("msg", "update host dep assign profiles responses received nil payload") + return nil + } + + // we expect all devices to success so pre-allocate just the success slice + success := make([]string, 0, len(payload.Devices)) + var ( + notAccessible []string + failed []string + ) + + for serial, status := range payload.Devices { + switch status { + case string(fleet.DEPAssignProfileResponseSuccess): + success = append(success, serial) + case string(fleet.DEPAssignProfileResponseNotAccessible): + notAccessible = append(notAccessible, serial) + case string(fleet.DEPAssignProfileResponseFailed): + failed = append(failed, serial) + default: + // this should never happen unless Apple changes the response format, so we log it for + // future debugging + level.Debug(ds.logger).Log("msg", "unrecognized assign profile response", "serial", serial, "status", status) + } + } + + return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, success, string(fleet.DEPAssignProfileResponseSuccess)); err != nil { + return err + } + if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, notAccessible, string(fleet.DEPAssignProfileResponseNotAccessible)); err != nil { + return err + } + if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, failed, string(fleet.DEPAssignProfileResponseFailed)); err != nil { + return err + } + return nil + }) +} + +func updateHostDEPAssignProfileResponses(ctx context.Context, tx sqlx.ExtContext, logger log.Logger, profileUUID string, serials []string, status string) error { + if len(serials) == 0 { + return nil + } + + stmt := ` +UPDATE + host_dep_assignments +JOIN + hosts ON id = host_id +SET + profile_uuid = ?, + assign_profile_response = ?, + response_updated_at = CURRENT_TIMESTAMP, + retry_job_id = 0 +WHERE + hardware_serial IN (?) +` + stmt, args, err := sqlx.In(stmt, profileUUID, status, serials) + if err != nil { + return ctxerr.Wrap(ctx, err, "prepare statement arguments") + } + res, err := tx.ExecContext(ctx, stmt, args...) + if err != nil { + return ctxerr.Wrap(ctx, err, "update host dep assignments") + } + + n, _ := res.RowsAffected() + level.Info(logger).Log("msg", "update host dep assign profile responses", "profile_uuid", profileUUID, "status", status, "devices", n, "serials", fmt.Sprintf("%s", serials)) + + return nil +} + +// depCooldownPeriod is the waiting period following a failed DEP assign profile request for a host. +const depCooldownPeriod = 1 * time.Hour // TODO: Make this a test config option? + +func (ds *Datastore) ScreenDEPAssignProfileSerialsForCooldown(ctx context.Context, serials []string) (skipSerials []string, assignSerials []string, err error) { + stmt := ` +SELECT + CASE WHEN assign_profile_response = ? AND (response_updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND) OR retry_job_id != 0) THEN + 'skip' + ELSE + 'assign' + END AS status, + hardware_serial +FROM + host_dep_assignments + JOIN hosts ON id = host_id +WHERE + hardware_serial IN (?) +` + + stmt, args, err := sqlx.In(stmt, string(fleet.DEPAssignProfileResponseFailed), depCooldownPeriod.Seconds(), serials) + if err != nil { + return nil, nil, ctxerr.Wrap(ctx, err, "screen dep serials: prepare statement arguments") + } + + var rows []struct { + Status string `db:"status"` + HardwareSerial string `db:"hardware_serial"` + } + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, args...); err != nil { + return nil, nil, ctxerr.Wrap(ctx, err, "screen dep serials: get rows") + } + + for _, r := range rows { + switch r.Status { + case "assign": + assignSerials = append(assignSerials, r.HardwareSerial) + case "skip": + skipSerials = append(skipSerials, r.HardwareSerial) + default: + return nil, nil, ctxerr.New(ctx, fmt.Sprintf("screen dep serials: %s unrecognized status: %s", r.HardwareSerial, r.Status)) + } + } + + return skipSerials, assignSerials, nil +} + +func (ds *Datastore) GetDEPAssignProfileExpiredCooldowns(ctx context.Context) (map[uint][]string, error) { + const stmt = ` +SELECT + COALESCE(team_id, 0) AS team_id, + hardware_serial +FROM + host_dep_assignments + JOIN hosts h ON h.id = host_id + LEFT JOIN jobs j ON j.id = retry_job_id +WHERE + assign_profile_response = ? + AND(retry_job_id = 0 OR j.state = ?) + AND(response_updated_at IS NULL + OR response_updated_at <= DATE_SUB(NOW(), INTERVAL ? SECOND))` + + var rows []struct { + TeamID uint `db:"team_id"` + HardwareSerial string `db:"hardware_serial"` + } + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, string(fleet.DEPAssignProfileResponseFailed), string(fleet.JobStateFailure), depCooldownPeriod.Seconds()); err != nil { + return nil, ctxerr.Wrap(ctx, err, "get host dep assign profile expired cooldowns") + } + + serialsByTeamID := make(map[uint][]string, len(rows)) + for _, r := range rows { + serialsByTeamID[r.TeamID] = append(serialsByTeamID[r.TeamID], r.HardwareSerial) + } + return serialsByTeamID, nil +} + +func (ds *Datastore) UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error { + if len(serials) == 0 { + return nil + } + + stmt := ` +UPDATE + host_dep_assignments +JOIN + hosts ON id = host_id +SET + retry_job_id = ? +WHERE + hardware_serial IN (?)` + + stmt, args, err := sqlx.In(stmt, jobID, serials) + if err != nil { + return ctxerr.Wrap(ctx, err, "prepare statement arguments") + } + + res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...) + if err != nil { + return ctxerr.Wrap(ctx, err, "update dep assign profile retry pending") + } + + n, _ := res.RowsAffected() + level.Info(ds.logger).Log("msg", "update dep assign profile retry pending", "job_id", jobID, "devices", n, "serials", fmt.Sprintf("%s", serials)) + + return nil +} + func (ds *Datastore) ResetMDMAppleEnrollment(ctx context.Context, hostUUID string) error { return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { // it's okay if we didn't update any rows, `nano_enrollments` entries diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 05c2d2f319..2fb6175529 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -755,6 +755,11 @@ const hostMDMSelect = `, WHEN hmdm.enrolled = 0 AND hmdm.installed_from_dep = 0 THEN 'Off' ELSE NULL END, + 'dep_profile_error', + CASE + WHEN hdep.assign_profile_response = '` + string(fleet.DEPAssignProfileResponseFailed) + `' THEN CAST(TRUE AS JSON) + ELSE CAST(FALSE AS JSON) + END, 'server_url', CASE WHEN hmdm.is_server = 1 THEN NULL @@ -798,6 +803,7 @@ const hostMDMJoin = ` LEFT JOIN mobile_device_management_solutions mdms ON hm.mdm_id = mdms.id ) hmdm ON hmdm.host_id = h.id LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id + LEFT JOIN host_dep_assignments hdep ON hdep.host_id = h.id ` func amountEnrolledHostsByOSDB(ctx context.Context, db sqlx.QueryerContext) (byOS map[string][]fleet.HostsCountByOSVersion, totalCount int, err error) { diff --git a/server/datastore/mysql/migrations/tables/20240228082706_AddHostDepAssignProfileResponses.go b/server/datastore/mysql/migrations/tables/20240228082706_AddHostDepAssignProfileResponses.go new file mode 100644 index 0000000000..936c1d857c --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240228082706_AddHostDepAssignProfileResponses.go @@ -0,0 +1,39 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20240228082706, Down_20240228082706) +} + +func Up_20240228082706(tx *sql.Tx) error { + stmt := ` +ALTER TABLE host_dep_assignments + -- profile_uuid is the uuid of the enrollment profile that was assigned to the host (which should correspond to an entry in the mdm_apple_setup_assistants table) + ADD COLUMN profile_uuid VARCHAR(37) COLLATE utf8mb4_unicode_ci NULL, + + -- assign_profile_response is the response received for the DEP profile assignment request (e.g., 'SUCCESS', 'NOT_ACCESSIBLE', or 'FAILED') + ADD COLUMN assign_profile_response VARCHAR(15) COLLATE utf8mb4_unicode_ci NULL, + + -- response_updated_at is the time the most recent DEP profile assignment response was received + ADD COLUMN response_updated_at TIMESTAMP NULL, + + -- retry_job_id is the id of job to retry a failed DEP profile assignment + ADD COLUMN retry_job_id int(10) UNSIGNED NOT NULL DEFAULT 0, + + ADD INDEX idx_hdep_response (assign_profile_response, response_updated_at);` + + if _, err := tx.Exec(stmt); err != nil { + return errors.Wrap(err, "alter host_dep_assignments table") + } + + return nil +} + +func Down_20240228082706(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240228082706_AddHostDepAssignProfileResponses_test.go b/server/datastore/mysql/migrations/tables/20240228082706_AddHostDepAssignProfileResponses_test.go new file mode 100644 index 0000000000..6668a4d518 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240228082706_AddHostDepAssignProfileResponses_test.go @@ -0,0 +1,54 @@ +package tables + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestUp_20240228082706(t *testing.T) { + db := applyUpToPrev(t) + insertStmt := "INSERT INTO host_dep_assignments (host_id) VALUES (?);" + execNoErr(t, db, insertStmt, 1337) + + // Apply current migration. + applyNext(t, db) + + // profile_uuid and assign_profile_response are now present and NULL + type hda struct { + HostID uint `db:"host_id"` + AddedAt time.Time `db:"added_at"` + DeletedAt *time.Time `db:"deleted_at"` + ProfileUUID *string `db:"profile_uuid"` + AssignProfileResponse *string `db:"assign_profile_response"` + ResponseUpdatedAt *time.Time `db:"response_updated_at"` + RetryJobID uint `db:"retry_job_id"` + } + var dest hda + err := sqlx.Get(db, &dest, `SELECT host_id, added_at, deleted_at, profile_uuid, assign_profile_response, response_updated_at, retry_job_id FROM host_dep_assignments WHERE host_id = ?`, 1337) + require.NoError(t, err) + require.Equal(t, uint(1337), dest.HostID) + require.NotZero(t, dest.AddedAt) + require.Nil(t, dest.DeletedAt) + require.Nil(t, dest.ProfileUUID) + require.Nil(t, dest.AssignProfileResponse) + require.Nil(t, dest.ResponseUpdatedAt) + require.Zero(t, dest.RetryJobID) + + // set profile_uuid and assign_profile_response to non-NULL values + execNoErr(t, db, `UPDATE host_dep_assignments SET profile_uuid = 'foo', assign_profile_response = 'bar', response_updated_at = NOW() WHERE host_id = ?`, 1337) + + dest = hda{} + err = sqlx.Get(db, &dest, `SELECT host_id, added_at, deleted_at, profile_uuid, assign_profile_response, response_updated_at, retry_job_id FROM host_dep_assignments WHERE host_id = ?`, 1337) + require.NoError(t, err) + require.Equal(t, uint(1337), dest.HostID) + require.NotZero(t, dest.AddedAt) + require.Nil(t, dest.DeletedAt) + require.Equal(t, "foo", *dest.ProfileUUID) + require.Equal(t, "bar", *dest.AssignProfileResponse) + require.NotNil(t, dest.ResponseUpdatedAt) + require.NotZero(t, dest.ResponseUpdatedAt) + require.Zero(t, dest.RetryJobID) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index c4bcc490b6..94ab9bb93d 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -196,7 +196,12 @@ CREATE TABLE `host_dep_assignments` ( `host_id` int(10) unsigned NOT NULL, `added_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `deleted_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`host_id`) + `profile_uuid` varchar(37) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `assign_profile_response` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `response_updated_at` timestamp NULL DEFAULT NULL, + `retry_job_id` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`host_id`), + KEY `idx_hdep_response` (`assign_profile_response`,`response_updated_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; @@ -770,9 +775,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=252 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=253 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index a76465f5bb..7f590fbc56 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -442,6 +442,14 @@ func (h *HostDEPAssignment) IsDEPAssignedToFleet() bool { return h.HostID > 0 && !h.AddedAt.IsZero() && h.DeletedAt == nil } +type DEPAssignProfileResponseStatus string + +const ( + DEPAssignProfileResponseSuccess DEPAssignProfileResponseStatus = "SUCCESS" + DEPAssignProfileResponseNotAccessible DEPAssignProfileResponseStatus = "NOT_ACCESSIBLE" + DEPAssignProfileResponseFailed DEPAssignProfileResponseStatus = "FAILED" +) + // NanoEnrollment represents a row in the nano_enrollments table managed by // nanomdm. It is meant to be used internally by the server, not to be returned // as part of endpoints, and as a precaution its json-encoding is explicitly diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 001349d46f..b6400cc5e3 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1132,6 +1132,21 @@ type Datastore interface { // host_dep_assignments for host with matching serials. DeleteHostDEPAssignments(ctx context.Context, serials []string) error + // UpdateHostDEPAssignProfileResponses receives a profile UUID and threes lists of serials, each representing + // one of the three possible responses, and updates the host_dep_assignments table with the corresponding responses. + UpdateHostDEPAssignProfileResponses(ctx context.Context, resp *godep.ProfileResponse) error + + // ScreenDEPAssignProfileSerialsForCooldown returns the serials that are still in cooldown and the + // ones that are ready to be assigned a profile. If `screenRetryJobs` is true, it will also skip + // any serials that have a non-zero `retry_job_id`. + ScreenDEPAssignProfileSerialsForCooldown(ctx context.Context, serials []string) (skipSerials []string, assignSerials []string, err error) + // GetDEPAssignProfileExpiredCooldowns returns the serials of the hosts that have expired + // cooldowns, grouped by team. + GetDEPAssignProfileExpiredCooldowns(ctx context.Context) (map[uint][]string, error) + // UpdateDEPAssignProfileRetryPending sets the retry_pending flag for the hosts with the given + // serials. + UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error + /////////////////////////////////////////////////////////////////////////////// // Microsoft MDM diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index e483cd555f..4623d244c7 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -364,6 +364,10 @@ type MDMHostData struct { // EnrollmentStatus is a string representation of state derived from // booleans stored in the host_mdm table, loaded by JOIN in datastore EnrollmentStatus *string `json:"enrollment_status" db:"-" csv:"mdm.enrollment_status"` + // DEPProfileError is a boolean representing whether Fleet received a "FAILED" response when + // attempting to assign a DEP profile for the host. + // See https://developer.apple.com/documentation/devicemanagement/assignprofileresponse + DEPProfileError bool `json:"dep_profile_error" db:"dep_profile_error" csv:"mdm.dep_profile_error"` // ServerURL is the server_url stored in the host_mdm table, loaded by // JOIN in datastore ServerURL *string `json:"server_url" db:"-" csv:"mdm.server_url"` diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index c777214530..b473e71b51 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -516,11 +516,26 @@ func (d *DEPService) processDeviceResponse(ctx context.Context, depClient *godep for profUUID, serials := range profileToSerials { logger := kitlog.With(d.logger, "profile_uuid", profUUID) level.Info(logger).Log("msg", "calling DEP client to assign profile", "profile_uuid", profUUID) - apiResp, err := depClient.AssignProfile(ctx, DEPName, profUUID, serials...) + + skipSerials, assignSerials, err := d.ds.ScreenDEPAssignProfileSerialsForCooldown(ctx, serials) + if err != nil { + return ctxerr.Wrap(ctx, err, "process device response") + } + if len(skipSerials) > 0 { + // NOTE: the `dep_cooldown` job of the `integrations`` cron picks up the assignments + // after the cooldown period is over + level.Debug(logger).Log("msg", "process device response: skipping assign profile for devices on cooldown", "serials", fmt.Sprintf("%s", skipSerials)) + } + if len(assignSerials) == 0 { + level.Debug(logger).Log("msg", "process device response: no devices to assign profile") + continue + } + + apiResp, err := depClient.AssignProfile(ctx, DEPName, profUUID, assignSerials...) if err != nil { level.Info(logger).Log( "msg", "assign profile", - "devices", len(serials), + "devices", len(assignSerials), "err", err, ) return fmt.Errorf("assign profile: %w", err) @@ -528,16 +543,14 @@ func (d *DEPService) processDeviceResponse(ctx context.Context, depClient *godep logs := []interface{}{ "msg", "profile assigned", - "devices", len(serials), + "devices", len(assignSerials), } logs = append(logs, logCountsForResults(apiResp.Devices)...) level.Info(logger).Log(logs...) - debugLogs := []interface{}{"msg", "assign profile responses by device"} - for k, v := range apiResp.Devices { - debugLogs = append(debugLogs, k, v) + if err := d.ds.UpdateHostDEPAssignProfileResponses(ctx, apiResp); err != nil { + return ctxerr.Wrap(ctx, err, "update host dep assign profile responses") } - level.Debug(logger).Log(debugLogs...) } return nil diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 123d878801..ecc7a4dff7 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -426,8 +426,6 @@ type NewGlobalPolicyFunc func(ctx context.Context, authorID *uint, args fleet.Po type PolicyFunc func(ctx context.Context, id uint) (*fleet.Policy, error) -type PolicyByNameFunc func(ctx context.Context, name string) (*fleet.Policy, error) - type SavePolicyFunc func(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool) error type ListGlobalPoliciesFunc func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) @@ -748,6 +746,14 @@ type GetMatchingHostSerialsFunc func(ctx context.Context, serials []string) (map type DeleteHostDEPAssignmentsFunc func(ctx context.Context, serials []string) error +type UpdateHostDEPAssignProfileResponsesFunc func(ctx context.Context, resp *godep.ProfileResponse) error + +type ScreenDEPAssignProfileSerialsForCooldownFunc func(ctx context.Context, serials []string) (skipSerials []string, assignSerials []string, err error) + +type GetDEPAssignProfileExpiredCooldownsFunc func(ctx context.Context) (map[uint][]string, error) + +type UpdateDEPAssignProfileRetryPendingFunc func(ctx context.Context, jobID uint, serials []string) error + type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error type WSTEPNewSerialFunc func(ctx context.Context) (*big.Int, error) @@ -1453,9 +1459,6 @@ type DataStore struct { PolicyFunc PolicyFunc PolicyFuncInvoked bool - PolicyByNameFunc PolicyByNameFunc - PolicyByNameFuncInvoked bool - SavePolicyFunc SavePolicyFunc SavePolicyFuncInvoked bool @@ -1936,6 +1939,18 @@ type DataStore struct { DeleteHostDEPAssignmentsFunc DeleteHostDEPAssignmentsFunc DeleteHostDEPAssignmentsFuncInvoked bool + UpdateHostDEPAssignProfileResponsesFunc UpdateHostDEPAssignProfileResponsesFunc + UpdateHostDEPAssignProfileResponsesFuncInvoked bool + + ScreenDEPAssignProfileSerialsForCooldownFunc ScreenDEPAssignProfileSerialsForCooldownFunc + ScreenDEPAssignProfileSerialsForCooldownFuncInvoked bool + + GetDEPAssignProfileExpiredCooldownsFunc GetDEPAssignProfileExpiredCooldownsFunc + GetDEPAssignProfileExpiredCooldownsFuncInvoked bool + + UpdateDEPAssignProfileRetryPendingFunc UpdateDEPAssignProfileRetryPendingFunc + UpdateDEPAssignProfileRetryPendingFuncInvoked bool + WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc WSTEPStoreCertificateFuncInvoked bool @@ -3505,13 +3520,6 @@ func (s *DataStore) Policy(ctx context.Context, id uint) (*fleet.Policy, error) return s.PolicyFunc(ctx, id) } -func (s *DataStore) PolicyByName(ctx context.Context, name string) (*fleet.Policy, error) { - s.mu.Lock() - s.PolicyByNameFuncInvoked = true - s.mu.Unlock() - return s.PolicyByNameFunc(ctx, name) -} - func (s *DataStore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool) error { s.mu.Lock() s.SavePolicyFuncInvoked = true @@ -4632,6 +4640,34 @@ func (s *DataStore) DeleteHostDEPAssignments(ctx context.Context, serials []stri return s.DeleteHostDEPAssignmentsFunc(ctx, serials) } +func (s *DataStore) UpdateHostDEPAssignProfileResponses(ctx context.Context, resp *godep.ProfileResponse) error { + s.mu.Lock() + s.UpdateHostDEPAssignProfileResponsesFuncInvoked = true + s.mu.Unlock() + return s.UpdateHostDEPAssignProfileResponsesFunc(ctx, resp) +} + +func (s *DataStore) ScreenDEPAssignProfileSerialsForCooldown(ctx context.Context, serials []string) (skipSerials []string, assignSerials []string, err error) { + s.mu.Lock() + s.ScreenDEPAssignProfileSerialsForCooldownFuncInvoked = true + s.mu.Unlock() + return s.ScreenDEPAssignProfileSerialsForCooldownFunc(ctx, serials) +} + +func (s *DataStore) GetDEPAssignProfileExpiredCooldowns(ctx context.Context) (map[uint][]string, error) { + s.mu.Lock() + s.GetDEPAssignProfileExpiredCooldownsFuncInvoked = true + s.mu.Unlock() + return s.GetDEPAssignProfileExpiredCooldownsFunc(ctx) +} + +func (s *DataStore) UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error { + s.mu.Lock() + s.UpdateDEPAssignProfileRetryPendingFuncInvoked = true + s.mu.Unlock() + return s.UpdateDEPAssignProfileRetryPendingFunc(ctx, jobID, serials) +} + func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error { s.mu.Lock() s.WSTEPStoreCertificateFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 73c6a19479..773129f307 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2787,7 +2787,7 @@ func (svc *Service) restorePendingDEPHost(ctx context.Context, host *fleet.Host, return ctxerr.Wrap(ctx, err, "restore pending dep host") } - if err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, + if _, err := worker.QueueMacosSetupAssistantJob(ctx, svc.ds, svc.logger, worker.MacosSetupAssistantHostsTransferred, tmID, host.HardwareSerial); err != nil { return ctxerr.Wrap(ctx, err, "restore pending dep host") } diff --git a/server/service/hosts.go b/server/service/hosts.go index db6e5baaec..db04996508 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -775,7 +775,7 @@ func (svc *Service) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs [] return ctxerr.Wrap(ctx, err, "list mdm dep serials in host ids") } if len(serials) > 0 { - if err := worker.QueueMacosSetupAssistantJob( + if _, err := worker.QueueMacosSetupAssistantJob( ctx, svc.ds, svc.logger, @@ -911,7 +911,7 @@ func (svc *Service) AddHostsToTeamByFilter(ctx context.Context, teamID *uint, op return ctxerr.Wrap(ctx, err, "list mdm dep serials in host ids") } if len(serials) > 0 { - if err := worker.QueueMacosSetupAssistantJob( + if _, err := worker.QueueMacosSetupAssistantJob( ctx, svc.ds, svc.logger, diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 9614bd5088..8e275e96ac 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -7047,7 +7047,7 @@ func (s *integrationTestSuite) TestHostsReportDownload() { res.Body.Close() require.NoError(t, err) require.Len(t, rows, len(hosts)+1) // all hosts + header row - assert.Len(t, rows[0], 50) // total number of cols + assert.Len(t, rows[0], 51) // total number of cols const ( idCol = 3 @@ -9149,7 +9149,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() { res.Body.Close() require.NoError(t, err) require.Len(t, rows1, len(hosts)+1) // all hosts + header row - assert.Len(t, rows1[0], 50) // total number of cols + assert.Len(t, rows1[0], 51) // total number of cols var ( idIdx int @@ -9176,7 +9176,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() { res.Body.Close() require.NoError(t, err) require.Len(t, rows2, len(hosts)+1) // all hosts + header row - assert.Len(t, rows2[0], 50) // total number of cols + assert.Len(t, rows2[0], 51) // total number of cols // Check that all hosts have 0 issues and that they match the previous call to `/hosts/report`. for i := 1; i < len(hosts)+1; i++ { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 1c08a55e88..aba846f09e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -75,17 +75,19 @@ func TestIntegrationsMDM(t *testing.T) { type integrationMDMTestSuite struct { suite.Suite withServer - fleetCfg config.FleetConfig - fleetDMNextCSRStatus atomic.Value - pushProvider *mock.APNSPushProvider - depStorage nanodep_storage.AllDEPStorage - depSchedule *schedule.Schedule - profileSchedule *schedule.Schedule - onProfileJobDone func() // function called when profileSchedule.Trigger() job completed - onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed - mdmStorage *mysql.NanoMDMStorage - worker *worker.Worker - mdmCommander *apple_mdm.MDMAppleCommander + fleetCfg config.FleetConfig + fleetDMNextCSRStatus atomic.Value + pushProvider *mock.APNSPushProvider + depStorage nanodep_storage.AllDEPStorage + depSchedule *schedule.Schedule + profileSchedule *schedule.Schedule + integrationsSchedule *schedule.Schedule + onProfileJobDone func() // function called when profileSchedule.Trigger() job completed + onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed + onIntegrationsScheduleDone func() // function called when integrationsSchedule.Trigger() job completed + mdmStorage *mysql.NanoMDMStorage + worker *worker.Worker + mdmCommander *apple_mdm.MDMAppleCommander } func (s *integrationMDMTestSuite) SetupSuite() { @@ -126,7 +128,24 @@ func (s *integrationMDMTestSuite) SetupSuite() { redisPool := redistest.SetupRedis(s.T(), "zz", false, false, false) s.withServer.lq = live_query_mock.New(s.T()) + macosJob := &worker.MacosSetupAssistant{ + Datastore: s.ds, + Log: kitlog.NewJSONLogger(os.Stdout), + DEPService: apple_mdm.NewDEPService(s.ds, depStorage, kitlog.NewJSONLogger(os.Stdout)), + DEPClient: apple_mdm.NewDEPClient(depStorage, s.ds, kitlog.NewJSONLogger(os.Stdout)), + } + appleMDMJob := &worker.AppleMDM{ + Datastore: s.ds, + Log: kitlog.NewJSONLogger(os.Stdout), + Commander: mdmCommander, + } + workr := worker.NewWorker(s.ds, kitlog.NewJSONLogger(os.Stdout)) + workr.TestIgnoreUnknownJobs = true + workr.Register(macosJob, appleMDMJob) + s.worker = workr + var depSchedule *schedule.Schedule + var integrationsSchedule *schedule.Schedule var profileSchedule *schedule.Schedule config := TestServerOpts{ License: &fleet.LicenseInfo{ @@ -187,6 +206,27 @@ func (s *integrationMDMTestSuite) SetupSuite() { return profileSchedule, nil } }, + func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc { + return func() (fleet.CronSchedule, error) { + const name = string(fleet.CronWorkerIntegrations) + logger := kitlog.NewJSONLogger(os.Stdout) + integrationsSchedule = schedule.New( + ctx, name, s.T().Name(), 1*time.Minute, ds, ds, + schedule.WithLogger(logger), + schedule.WithJob("integrations_worker", func(ctx context.Context) error { + return s.worker.ProcessJobs(ctx) + }), + schedule.WithJob("dep_cooldowns", func(ctx context.Context) error { + if s.onIntegrationsScheduleDone != nil { + defer s.onIntegrationsScheduleDone() + } + + return worker.ProcessDEPCooldowns(ctx, ds, logger) + }), + ) + return integrationsSchedule, nil + } + }, }, APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589", } @@ -199,26 +239,11 @@ func (s *integrationMDMTestSuite) SetupSuite() { s.pushProvider = pushProvider s.depStorage = depStorage s.depSchedule = depSchedule + s.integrationsSchedule = integrationsSchedule s.profileSchedule = profileSchedule s.mdmStorage = mdmStorage s.mdmCommander = mdmCommander - macosJob := &worker.MacosSetupAssistant{ - Datastore: s.ds, - Log: kitlog.NewJSONLogger(os.Stdout), - DEPService: apple_mdm.NewDEPService(s.ds, depStorage, kitlog.NewJSONLogger(os.Stdout)), - DEPClient: apple_mdm.NewDEPClient(depStorage, s.ds, kitlog.NewJSONLogger(os.Stdout)), - } - appleMDMJob := &worker.AppleMDM{ - Datastore: s.ds, - Log: kitlog.NewJSONLogger(os.Stdout), - Commander: mdmCommander, - } - workr := worker.NewWorker(s.ds, kitlog.NewJSONLogger(os.Stdout)) - workr.TestIgnoreUnknownJobs = true - workr.Register(macosJob, appleMDMJob) - s.worker = workr - fleetdmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { status := s.fleetDMNextCSRStatus.Swap(http.StatusOK) w.WriteHeader(status.(int)) @@ -278,12 +303,19 @@ func (s *integrationMDMTestSuite) TearDownTest() { _, err := q.ExecContext(ctx, "DELETE FROM mdm_windows_configuration_profiles") return err }) + // clear any pending worker job mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, "DELETE FROM jobs") return err }) + // clear any host dep assignments + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, "DELETE FROM host_dep_assignments") + return err + }) + // clear any mdm windows enrollments mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, "DELETE FROM mdm_windows_enrollments") @@ -1973,6 +2005,104 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { } } + checkAssignProfileRequests := func(serial string, profUUID *string) { + require.NotEmpty(t, profileAssignmentReqs) + require.Len(t, profileAssignmentReqs, 1) + require.Len(t, profileAssignmentReqs[0].Devices, 1) + require.Equal(t, serial, profileAssignmentReqs[0].Devices[0]) + if profUUID != nil { + require.Equal(t, *profUUID, profileAssignmentReqs[0].ProfileUUID) + } + } + + type hostDEPRow struct { + HostID uint `db:"host_id"` + ProfileUUID string `db:"profile_uuid"` + AssignProfileResponse string `db:"assign_profile_response"` + ResponseUpdatedAt time.Time `db:"response_updated_at"` + RetryJobID uint `db:"retry_job_id"` + } + checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow { + bySerial := make(map[string]hostDEPRow, len(deviceSerials)) + for _, deviceSerial := range deviceSerials { + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + var dest hostDEPRow + err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial) + require.NoError(t, err) + require.Equal(t, string(expectedStatus), dest.AssignProfileResponse) + bySerial[deviceSerial] = dest + return nil + }) + } + return bySerial + } + + checkPendingMacOSSetupAssistantJob := func(expectedTask string, expectedTeamID *uint, expectedSerials []string, expectedJobID uint) { + pending, err := s.ds.GetQueuedJobs(context.Background(), 1) + require.NoError(t, err) + require.Len(t, pending, 1) + require.Equal(t, "macos_setup_assistant", pending[0].Name) + require.NotNil(t, pending[0].Args) + var gotArgs struct { + Task string `json:"task"` + TeamID *uint `json:"team_id,omitempty"` + HostSerialNumbers []string `json:"host_serial_numbers,omitempty"` + } + require.NoError(t, json.Unmarshal(*pending[0].Args, &gotArgs)) + require.Equal(t, expectedTask, gotArgs.Task) + if expectedTeamID != nil { + require.NotNil(t, gotArgs.TeamID) + require.Equal(t, *expectedTeamID, *gotArgs.TeamID) + } else { + require.Nil(t, gotArgs.TeamID) + } + require.Equal(t, expectedSerials, gotArgs.HostSerialNumbers) + + if expectedJobID != 0 { + require.Equal(t, expectedJobID, pending[0].ID) + } + } + + checkNoJobsPending := func() { + pending, err := s.ds.GetQueuedJobs(context.Background(), 1) + require.NoError(t, err) + require.Empty(t, pending) + } + + expectNoJobID := ptr.Uint(0) // used when expect no retry job + checkHostCooldown := func(serial, profUUID string, status fleet.DEPAssignProfileResponseStatus, expectUpdatedAt *time.Time, expectRetryJobID *uint) hostDEPRow { + bySerial := checkHostDEPAssignProfileResponses([]string{serial}, profUUID, status) + d, ok := bySerial[serial] + require.True(t, ok) + if expectUpdatedAt != nil { + require.Equal(t, *expectUpdatedAt, d.ResponseUpdatedAt) + } + if expectRetryJobID != nil { + require.Equal(t, *expectRetryJobID, d.RetryJobID) + } + return d + } + + checkListHostDEPError := func(serial string, expectStatus string, expectError bool) *fleet.HostResponse { + listHostsRes := listHostsResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts?query=%s", serial), nil, http.StatusOK, &listHostsRes) + require.Len(t, listHostsRes.Hosts, 1) + require.Equal(t, serial, listHostsRes.Hosts[0].HardwareSerial) + require.Equal(t, expectStatus, *listHostsRes.Hosts[0].MDM.EnrollmentStatus) + require.Equal(t, expectError, listHostsRes.Hosts[0].MDM.DEPProfileError) + + return &listHostsRes.Hosts[0] + } + + setAssignProfileResponseUpdatedAt := func(serial string, updatedAt time.Time) { + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE host_dep_assignments SET response_updated_at = ? WHERE host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)`, updatedAt, serial) + return err + }) + } + + expectAssignProfileResponseFailed := "" // set to device serial when testing the failed profile assignment flow + expectAssignProfileResponseNotAccessible := "" // set to device serial when testing the not accessible profile assignment flow s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) encoder := json.NewEncoder(w) @@ -1999,7 +2129,21 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { var prof profileAssignmentReq require.NoError(t, json.Unmarshal(b, &prof)) profileAssignmentReqs = append(profileAssignmentReqs, prof) - _, _ = w.Write([]byte(`{}`)) + var resp godep.ProfileResponse + resp.ProfileUUID = prof.ProfileUUID + resp.Devices = make(map[string]string, len(prof.Devices)) + for _, device := range prof.Devices { + switch device { + case expectAssignProfileResponseNotAccessible: + resp.Devices[device] = string(fleet.DEPAssignProfileResponseNotAccessible) + case expectAssignProfileResponseFailed: + resp.Devices[device] = string(fleet.DEPAssignProfileResponseFailed) + default: + resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess) + } + } + err = encoder.Encode(resp) + require.NoError(t, err) default: _, _ = w.Write([]byte(`{}`)) } @@ -2032,7 +2176,9 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { // - one when we do the device sync (/device/sync) require.Len(t, profileAssignmentReqs, 2) require.Len(t, profileAssignmentReqs[0].Devices, 1) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[0].Devices, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) require.Len(t, profileAssignmentReqs[1].Devices, len(devices)) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[1].Devices, profileAssignmentReqs[1].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) // create a new host nonDEPHost := createHostAndDeviceToken(t, s.ds, "not-dep") @@ -2156,6 +2302,7 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { // TODO: seems like we're doing this request on each loop? require.Len(t, profileAssignmentReqs[0].Devices, 1) require.Equal(t, devices[0].SerialNumber, profileAssignmentReqs[0].Devices[0]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[0].Devices, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) // profileAssignmentReqs[1] and [2] can be in any order ix2Devices, ix1Device := 1, 2 @@ -2166,12 +2313,14 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { // - existing device with "added" // - new device with "added" require.Len(t, profileAssignmentReqs[ix2Devices].Devices, 2, "%#+v", profileAssignmentReqs) - require.Equal(t, devices[0].SerialNumber, profileAssignmentReqs[ix2Devices].Devices[0]) + require.Equal(t, devices[0].SerialNumber, profileAssignmentReqs[ix2Devices].Devices[0]) // FIXME: this test seems to be flaky require.Equal(t, addedSerial, profileAssignmentReqs[ix2Devices].Devices[1]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[ix2Devices].Devices, profileAssignmentReqs[ix2Devices].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) // - existing device with "modified" and a different team (thus different profile request) require.Len(t, profileAssignmentReqs[ix1Device].Devices, 1) require.Equal(t, devices[1].SerialNumber, profileAssignmentReqs[ix1Device].Devices[0]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[ix1Device].Devices, profileAssignmentReqs[ix1Device].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) // entries for all hosts except for the one with OpType = "deleted" assignment, err := s.ds.GetHostDEPAssignment(ctx, deletedHostID) @@ -2205,6 +2354,7 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { profileAssignmentReqs = []profileAssignmentReq{} s.runWorker() require.Equal(t, mdmDevice.SerialNumber, profileAssignmentReqs[0].Devices[0]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[0].Devices, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) // it should get the post-enrollment commands require.NoError(t, mdmDevice.Enroll()) @@ -2272,6 +2422,11 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { s.runDEPSchedule() require.NotEmpty(t, profileAssignmentReqs) require.Equal(t, eHost.HardwareSerial, profileAssignmentReqs[0].Devices[0]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[0].Devices, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + + // report MDM info via osquery + require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, eHost.ID, false, true, s.server.URL, true, fleet.WellKnownMDMFleet, "")) + checkListHostDEPError(eHost.HardwareSerial, "On (automatic)", false) // transfer to "no team", we assign a DEP profile to the device profileAssignmentReqs = []profileAssignmentReq{} @@ -2280,6 +2435,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { s.runWorker() require.NotEmpty(t, profileAssignmentReqs) require.Equal(t, eHost.HardwareSerial, profileAssignmentReqs[0].Devices[0]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[0].Devices, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + checkListHostDEPError(eHost.HardwareSerial, "On (automatic)", false) // transfer to the team back again, we assign a DEP profile to the device again s.Do("POST", "/api/v1/fleet/hosts/transfer", @@ -2288,6 +2445,220 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { s.runWorker() require.NotEmpty(t, profileAssignmentReqs) require.Equal(t, eHost.HardwareSerial, profileAssignmentReqs[0].Devices[0]) + checkHostDEPAssignProfileResponses(profileAssignmentReqs[0].Devices, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) + checkListHostDEPError(eHost.HardwareSerial, "On (automatic)", false) + + // transfer to "no team", but simulate a failed profile assignment + expectAssignProfileResponseFailed = eHost.HardwareSerial + profileAssignmentReqs = []profileAssignmentReq{} + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: nil, HostIDs: []uint{eHost.ID}}, http.StatusOK) + checkPendingMacOSSetupAssistantJob("hosts_transferred", nil, []string{eHost.HardwareSerial}, 0) + + s.runIntegrationsSchedule() + checkAssignProfileRequests(eHost.HardwareSerial, nil) + profUUID := profileAssignmentReqs[0].ProfileUUID + d := checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, nil, expectNoJobID) + require.NotZero(t, d.ResponseUpdatedAt) + failedAt := d.ResponseUpdatedAt + checkNoJobsPending() + // list hosts shows dep profile error + checkListHostDEPError(eHost.HardwareSerial, "On (automatic)", true) + + // run the integrations schedule during the cooldown period + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // no new request during cooldown + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // create a new team + var tmResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{ + Name: t.Name() + "dummy", + Description: "desc dummy", + }, http.StatusOK, &tmResp) + require.NotZero(t, createTeamResp.Team.ID) + dummyTeam := tmResp.Team + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: &dummyTeam.ID, HostIDs: []uint{eHost.ID}}, http.StatusOK) + checkPendingMacOSSetupAssistantJob("hosts_transferred", &dummyTeam.ID, []string{eHost.HardwareSerial}, 0) + + // expect no assign profile request during cooldown + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // screened for cooldown + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // cooldown hosts are screened from update profile jobs that would assign profiles + _, err = worker.QueueMacosSetupAssistantJob(ctx, s.ds, kitlog.NewNopLogger(), worker.MacosSetupAssistantUpdateProfile, &dummyTeam.ID, eHost.HardwareSerial) + require.NoError(t, err) + checkPendingMacOSSetupAssistantJob("update_profile", &dummyTeam.ID, []string{eHost.HardwareSerial}, 0) + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // screened for cooldown + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // cooldown hosts are screened from delete profile jobs that would assign profiles + _, err = worker.QueueMacosSetupAssistantJob(ctx, s.ds, kitlog.NewNopLogger(), worker.MacosSetupAssistantProfileDeleted, &dummyTeam.ID, eHost.HardwareSerial) + require.NoError(t, err) + checkPendingMacOSSetupAssistantJob("profile_deleted", &dummyTeam.ID, []string{eHost.HardwareSerial}, 0) + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // screened for cooldown + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // // TODO: Restore this test when FIXME on DeleteTeam is addressed + // s.Do("DELETE", fmt.Sprintf("/api/v1/fleet/teams/%d", dummyTeam.ID), nil, http.StatusOK) + // checkPendingMacOSSetupAssistantJob("team_deleted", nil, []string{eHost.HardwareSerial}, 0) + // s.runIntegrationsSchedule() + // require.Empty(t, profileAssignmentReqs) // screened for cooldown + // bySerial = checkHostDEPAssignProfileResponses([]string{eHost.HardwareSerial}, profUUID, fleet.DEPAssignProfileResponseFailed) + // d, ok = bySerial[eHost.HardwareSerial] + // require.True(t, ok) + // require.Equal(t, failedAt, d.ResponseUpdatedAt) + // require.Zero(t, d.RetryJobID) // cooling down so no retry job + // checkNoJobsPending() + + // transfer back to no team, expect no assign profile request during cooldown + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: nil, HostIDs: []uint{eHost.ID}}, http.StatusOK) + checkPendingMacOSSetupAssistantJob("hosts_transferred", nil, []string{eHost.HardwareSerial}, 0) + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // screened for cooldown + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // simulate expired cooldown + failedAt = failedAt.Add(-2 * time.Hour) + setAssignProfileResponseUpdatedAt(eHost.HardwareSerial, failedAt) + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // assign profile request will be made when the retry job is processed on the next worker run + d = checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, nil) + require.NotZero(t, d.RetryJobID) // retry job created + jobID := d.RetryJobID + checkPendingMacOSSetupAssistantJob("hosts_cooldown", nil, []string{eHost.HardwareSerial}, jobID) + + // running the DEP schedule should not trigger a profile assignment request when the retry job is pending + profileAssignmentReqs = []profileAssignmentReq{} + s.runDEPSchedule() + require.Empty(t, profileAssignmentReqs) // assign profile request will be made when the retry job is processed on the next worker run + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, &jobID) // no change + checkPendingMacOSSetupAssistantJob("hosts_cooldown", nil, []string{eHost.HardwareSerial}, jobID) + checkListHostDEPError(eHost.HardwareSerial, "On (automatic)", true) + + // run the inregration schedule and expect success + expectAssignProfileResponseFailed = "" + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + checkAssignProfileRequests(eHost.HardwareSerial, &profUUID) + d = checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseSuccess, nil, expectNoJobID) // retry job cleared + require.True(t, d.ResponseUpdatedAt.After(failedAt)) + succeededAt := d.ResponseUpdatedAt + checkNoJobsPending() + checkListHostDEPError(eHost.HardwareSerial, "On (automatic)", false) + + // run the integrations schedule and expect no changes + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) + checkHostCooldown(eHost.HardwareSerial, profUUID, fleet.DEPAssignProfileResponseSuccess, &succeededAt, expectNoJobID) // no change + checkNoJobsPending() + + // ingest new device via DEP but the profile assignment fails + serial := uuid.NewString() + devices = []godep.Device{ + {SerialNumber: serial, Model: "MacBook Pro", OS: "osx", OpType: "added"}, + } + expectAssignProfileResponseFailed = serial + profileAssignmentReqs = []profileAssignmentReq{} + s.runDEPSchedule() + checkAssignProfileRequests(serial, nil) + profUUID = profileAssignmentReqs[0].ProfileUUID + d = checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseFailed, nil, expectNoJobID) + require.NotZero(t, d.ResponseUpdatedAt) + failedAt = d.ResponseUpdatedAt + checkNoJobsPending() + h := checkListHostDEPError(serial, "Pending", true) // list hosts shows device pending and dep profile error + + // transfer to team, no profile assignment request is made during the cooldown period + profileAssignmentReqs = []profileAssignmentReq{} + s.Do("POST", "/api/v1/fleet/hosts/transfer", + addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{h.ID}}, http.StatusOK) + checkPendingMacOSSetupAssistantJob("hosts_transferred", &team.ID, []string{serial}, 0) + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // screened by cooldown + checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // run the integrations schedule and expect no changes + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) + checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, expectNoJobID) // no change + checkNoJobsPending() + + // simulate expired cooldown + failedAt = failedAt.Add(-2 * time.Hour) + setAssignProfileResponseUpdatedAt(serial, failedAt) + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) // assign profile request will be made when the retry job is processed on the next worker run + d = checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseFailed, &failedAt, nil) + require.NotZero(t, d.RetryJobID) // retry job created + jobID = d.RetryJobID + checkPendingMacOSSetupAssistantJob("hosts_cooldown", &team.ID, []string{serial}, jobID) + + // run the inregration schedule and expect success + expectAssignProfileResponseFailed = "" + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + checkAssignProfileRequests(serial, nil) + require.NotEqual(t, profUUID, profileAssignmentReqs[0].ProfileUUID) // retry job will use the current team profile instead + profUUID = profileAssignmentReqs[0].ProfileUUID + d = checkHostCooldown(serial, profUUID, fleet.DEPAssignProfileResponseSuccess, nil, expectNoJobID) // retry job cleared + require.True(t, d.ResponseUpdatedAt.After(failedAt)) + checkNoJobsPending() + // list hosts shows pending (because MDM detail query hasn't been reported) but dep profile + // error has been cleared + checkListHostDEPError(serial, "Pending", false) + + // ingest another device via DEP but the profile assignment is not accessible + serial = uuid.NewString() + devices = []godep.Device{ + {SerialNumber: serial, Model: "MacBook Pro", OS: "osx", OpType: "added"}, + } + expectAssignProfileResponseNotAccessible = serial + profileAssignmentReqs = []profileAssignmentReq{} + s.runDEPSchedule() + require.Len(t, profileAssignmentReqs, 2) // FIXME: When new device is added in ABM, we see two profile assign requests when device is not accessible: first during the "fetch" phase, then during the "sync" phase + expectProfileUUID := "" + for _, req := range profileAssignmentReqs { + require.Len(t, req.Devices, 1) + require.Equal(t, serial, req.Devices[0]) + if expectProfileUUID == "" { + expectProfileUUID = req.ProfileUUID + } else { + require.Equal(t, expectProfileUUID, req.ProfileUUID) + } + d := checkHostCooldown(serial, req.ProfileUUID, fleet.DEPAssignProfileResponseNotAccessible, nil, expectNoJobID) // not accessible responses aren't retried + require.NotZero(t, d.ResponseUpdatedAt) + failedAt = d.ResponseUpdatedAt + } + // list hosts shows device pending and no dep profile error for not accessible responses + checkListHostDEPError(serial, "Pending", false) + + // no retry job for not accessible responses even if cooldown expires + failedAt = failedAt.Add(-2 * time.Hour) + setAssignProfileResponseUpdatedAt(serial, failedAt) + profileAssignmentReqs = []profileAssignmentReq{} + s.runIntegrationsSchedule() + require.Empty(t, profileAssignmentReqs) + checkHostCooldown(serial, expectProfileUUID, fleet.DEPAssignProfileResponseNotAccessible, &failedAt, expectNoJobID) // no change + checkNoJobsPending() } func loadEnrollmentProfileDEPToken(t *testing.T, ds *mysql.Datastore) string { @@ -9974,6 +10345,19 @@ func (s *integrationMDMTestSuite) runDEPSchedule() { <-ch } +func (s *integrationMDMTestSuite) runIntegrationsSchedule() { + // FIXME: This pattern (which is being used in testing other schedules as well) seems cause issues + // where a subsequent call attempts to trigger when the schedule's trigger channel is full and + // schedule ignored the subsquent call (which is the documented behavior of the trigger). + // In testing, this can cause the test to hang until the next scheduled run. It isn't a very + // noticeable issue here since the intervals for these schedules are short. + ch := make(chan bool) + s.onIntegrationsScheduleDone = func() { close(ch) } + _, err := s.integrationsSchedule.Trigger() + require.NoError(s.T(), err) + <-ch +} + func (s *integrationMDMTestSuite) getRawTokenValue(content string) string { // Create a regex object with the defined pattern pattern := `inputToken.value\s*=\s*'([^']*)'` diff --git a/server/worker/macos_setup_assistant.go b/server/worker/macos_setup_assistant.go index 3a24a7fcbe..cf206c9ff5 100644 --- a/server/worker/macos_setup_assistant.go +++ b/server/worker/macos_setup_assistant.go @@ -3,6 +3,7 @@ package worker import ( "context" "encoding/json" + "fmt" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -27,6 +28,7 @@ const ( MacosSetupAssistantHostsTransferred MacosSetupAssistantTask = "hosts_transferred" MacosSetupAssistantUpdateAllProfiles MacosSetupAssistantTask = "update_all_profiles" MacosSetupAssistantUpdateProfile MacosSetupAssistantTask = "update_profile" + MacosSetupAssistantHostsCooldown MacosSetupAssistantTask = "hosts_cooldown" ) // MacosSetupAssistant is the job processor for the macos_setup_assistant job. @@ -71,7 +73,9 @@ func (m *MacosSetupAssistant) Run(ctx context.Context, argsJSON json.RawMessage) case MacosSetupAssistantTeamDeleted: return m.runTeamDeleted(ctx, args) case MacosSetupAssistantHostsTransferred: - return m.runHostsTransferred(ctx, args) + return m.runHostsTransferred(ctx, args, false) + case MacosSetupAssistantHostsCooldown: + return m.runHostsTransferred(ctx, args, true) case MacosSetupAssistantUpdateAllProfiles: return m.runUpdateAllProfiles(ctx, args) case MacosSetupAssistantUpdateProfile: @@ -113,9 +117,27 @@ func (m *MacosSetupAssistant) runProfileChanged(ctx context.Context, args macosS return ctxerr.Wrap(ctx, err, "list mdm dep serials in team") } if len(serials) > 0 { - if _, err := m.DEPClient.AssignProfile(ctx, apple_mdm.DEPName, profUUID, serials...); err != nil { + skipSerials, assignSerials, err := m.Datastore.ScreenDEPAssignProfileSerialsForCooldown(ctx, serials) + if err != nil { + return ctxerr.Wrap(ctx, err, "run profile changed") + } + if len(skipSerials) > 0 { + // NOTE: the `dep_cooldown` job of the `integrations`` cron picks up the assignments + // after the cooldown period is over + level.Info(m.Log).Log("msg", "run profile changed: skipping assign profile for devices on cooldown", "serials", fmt.Sprintf("%s", skipSerials)) + } + if len(assignSerials) == 0 { + level.Info(m.Log).Log("msg", "run profile changed: no devices to assign profile") + return nil + } + + resp, err := m.DEPClient.AssignProfile(ctx, apple_mdm.DEPName, profUUID, assignSerials...) + if err != nil { return ctxerr.Wrap(ctx, err, "assign profile") } + if err := m.Datastore.UpdateHostDEPAssignProfileResponses(ctx, resp); err != nil { + return ctxerr.Wrap(ctx, err, "worker: run profile changed") + } } return nil } @@ -163,9 +185,27 @@ func (m *MacosSetupAssistant) runProfileDeleted(ctx context.Context, args macosS return ctxerr.Wrap(ctx, err, "list mdm dep serials in team") } if len(serials) > 0 { - if _, err := m.DEPClient.AssignProfile(ctx, apple_mdm.DEPName, profUUID, serials...); err != nil { + skipSerials, assignSerials, err := m.Datastore.ScreenDEPAssignProfileSerialsForCooldown(ctx, serials) + if err != nil { + return ctxerr.Wrap(ctx, err, "run profile deleted") + } + if len(skipSerials) > 0 { + // NOTE: the `dep_cooldown` job of the `integrations`` cron picks up the assignments + // after the cooldown period is over + level.Info(m.Log).Log("msg", "run profile deleted: skipping assign profile for devices on cooldown", "serials", fmt.Sprintf("%s", skipSerials)) + } + if len(assignSerials) == 0 { + level.Info(m.Log).Log("msg", "run profile deleted: no devices to assign profile") + return nil + } + + resp, err := m.DEPClient.AssignProfile(ctx, apple_mdm.DEPName, profUUID, assignSerials...) + if err != nil { return ctxerr.Wrap(ctx, err, "assign profile") } + if err := m.Datastore.UpdateHostDEPAssignProfileResponses(ctx, resp); err != nil { + return ctxerr.Wrap(ctx, err, "worker: run profile deleted") + } } return nil } @@ -173,10 +213,10 @@ func (m *MacosSetupAssistant) runProfileDeleted(ctx context.Context, args macosS func (m *MacosSetupAssistant) runTeamDeleted(ctx context.Context, args macosSetupAssistantArgs) error { // team deletion is semantically equivalent to moving hosts to "no team" args.TeamID = nil // should already be this way, but just to make sure - return m.runHostsTransferred(ctx, args) + return m.runHostsTransferred(ctx, args, false) } -func (m *MacosSetupAssistant) runHostsTransferred(ctx context.Context, args macosSetupAssistantArgs) error { +func (m *MacosSetupAssistant) runHostsTransferred(ctx context.Context, args macosSetupAssistantArgs, fromCooldown bool) error { team, err := m.getTeamNoTeam(ctx, args.TeamID) if err != nil { if fleet.IsNotFound(err) { @@ -205,10 +245,33 @@ func (m *MacosSetupAssistant) runHostsTransferred(ctx context.Context, args maco } } - _, err = m.DEPClient.AssignProfile(ctx, apple_mdm.DEPName, profUUID, args.HostSerialNumbers...) + serials := args.HostSerialNumbers + if !fromCooldown { + // if not a retry, then we need to screen the serials for cooldown + skipSerials, assignSerials, err := m.Datastore.ScreenDEPAssignProfileSerialsForCooldown(ctx, serials) + if err != nil { + return ctxerr.Wrap(ctx, err, "run hosts transferred") + } + if len(skipSerials) > 0 { + // NOTE: the `dep_cooldown` job of the `integrations` cron picks up the assignments + // after the cooldown period is over + level.Info(m.Log).Log("msg", "run hosts transferred: skipping assign profile for devices on cooldown", "serials", fmt.Sprintf("%s", skipSerials)) + } + serials = assignSerials + } + + if len(serials) == 0 { + level.Info(m.Log).Log("msg", "run hosts transferred: no devices to assign profile") + return nil + } + + resp, err := m.DEPClient.AssignProfile(ctx, apple_mdm.DEPName, profUUID, serials...) if err != nil { return ctxerr.Wrap(ctx, err, "assign profile") } + if err := m.Datastore.UpdateHostDEPAssignProfileResponses(ctx, resp); err != nil { + return ctxerr.Wrap(ctx, err, "worker: run hosts transferred") + } return nil } @@ -225,7 +288,7 @@ func (m *MacosSetupAssistant) runUpdateAllProfiles(ctx context.Context, args mac teamID = &team.ID } - if err := QueueMacosSetupAssistantJob(ctx, m.Datastore, m.Log, MacosSetupAssistantUpdateProfile, teamID); err != nil { + if _, err := QueueMacosSetupAssistantJob(ctx, m.Datastore, m.Log, MacosSetupAssistantUpdateProfile, teamID); err != nil { return ctxerr.Wrap(ctx, err, "queue macos setup assistant update profile job") } return nil @@ -254,7 +317,7 @@ func (m *MacosSetupAssistant) runUpdateProfile(ctx context.Context, args macosSe if fleet.IsNotFound(err) { // no setup assistant for that team, enqueue a profile deleted task so // the default profile is assigned to the hosts. - if err := QueueMacosSetupAssistantJob(ctx, m.Datastore, m.Log, MacosSetupAssistantProfileDeleted, args.TeamID); err != nil { + if _, err := QueueMacosSetupAssistantJob(ctx, m.Datastore, m.Log, MacosSetupAssistantProfileDeleted, args.TeamID); err != nil { return ctxerr.Wrap(ctx, err, "queue macos setup assistant profile deleted job") } return nil @@ -264,7 +327,7 @@ func (m *MacosSetupAssistant) runUpdateProfile(ctx context.Context, args macosSe // no error means that the setup assistant existed for that team, enqueue a profile // changed task so the custom profile is assigned to the hosts. - if err := QueueMacosSetupAssistantJob(ctx, m.Datastore, m.Log, MacosSetupAssistantProfileChanged, args.TeamID); err != nil { + if _, err := QueueMacosSetupAssistantJob(ctx, m.Datastore, m.Log, MacosSetupAssistantProfileChanged, args.TeamID); err != nil { return ctxerr.Wrap(ctx, err, "queue macos setup assistant profile changed job") } return nil @@ -291,7 +354,7 @@ func QueueMacosSetupAssistantJob( task MacosSetupAssistantTask, teamID *uint, serialNumbers ...string, -) error { +) (uint, error) { attrs := []interface{}{ "enabled", "true", macosSetupAssistantJobName, task, @@ -309,8 +372,47 @@ func QueueMacosSetupAssistantJob( } job, err := QueueJob(ctx, ds, macosSetupAssistantJobName, args) if err != nil { - return ctxerr.Wrap(ctx, err, "queueing job") + return 0, ctxerr.Wrap(ctx, err, "queueing job") } level.Debug(logger).Log("job_id", job.ID) + return job.ID, nil +} + +func ProcessDEPCooldowns(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) error { + serialsByTeamId, err := ds.GetDEPAssignProfileExpiredCooldowns(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting cooldowns") + } + if len(serialsByTeamId) == 0 { + logger.Log("msg", "no cooldowns to process") + return nil + } + + // queue job for each team so that macOS setup assistant worker can pick it up and process it + for teamID, serials := range serialsByTeamId { + if len(serials) == 0 { + logger.Log("msg", "no cooldowns", "team_id", teamID) + continue + } + logger.Log("msg", "processing cooldowns", "team_id", teamID, "serials", serials) + + var tid *uint + if teamID != 0 { + tid = &teamID + } + + id, err := QueueMacosSetupAssistantJob(ctx, ds, logger, + MacosSetupAssistantHostsCooldown, + tid, serials..., + ) + if err != nil { + return ctxerr.Wrap(ctx, err, "queue macos setup assistant job for cooldowns") + } + + if err := ds.UpdateDEPAssignProfileRetryPending(ctx, id, serials); err != nil { + return ctxerr.Wrap(ctx, err, "updating dep assign profile retry pending") + } + + } return nil } diff --git a/server/worker/macos_setup_assistant_test.go b/server/worker/macos_setup_assistant_test.go index 304f10ebb9..e2633363bf 100644 --- a/server/worker/macos_setup_assistant_test.go +++ b/server/worker/macos_setup_assistant_test.go @@ -58,7 +58,7 @@ func TestMacosSetupAssistant(t *testing.T) { err = ds.AddHostsToTeam(ctx, &tm2.ID, []uint{hosts[4].ID, hosts[5].ID}) require.NoError(t, err) - var testBMToken = nanodep_client.OAuth1Tokens{ + testBMToken := nanodep_client.OAuth1Tokens{ ConsumerKey: "test_consumer", ConsumerSecret: "test_secret", AccessToken: "test_access_token", @@ -155,7 +155,7 @@ func TestMacosSetupAssistant(t *testing.T) { start := time.Now().Truncate(time.Second) // enqueue a regenerate all and process the jobs - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateAllProfiles, nil) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateAllProfiles, nil) require.NoError(t, err) runCheckDone() @@ -188,7 +188,7 @@ func TestMacosSetupAssistant(t *testing.T) { }) require.NoError(t, err) require.NotZero(t, tm1Asst.ID) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, &tm1.ID) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, &tm1.ID) require.NoError(t, err) runCheckDone() @@ -219,7 +219,7 @@ func TestMacosSetupAssistant(t *testing.T) { tm2, err = ds.SaveTeam(ctx, tm2) require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateProfile, &tm2.ID) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateProfile, &tm2.ID) require.NoError(t, err) runCheckDone() @@ -249,11 +249,11 @@ func TestMacosSetupAssistant(t *testing.T) { require.NotZero(t, tm3Asst.ID) err = ds.DeleteMDMAppleSetupAssistant(ctx, &tm1.ID) require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, &tm2.ID) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, &tm2.ID) require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, &tm3.ID) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, &tm3.ID) require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileDeleted, &tm1.ID) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileDeleted, &tm1.ID) require.NoError(t, err) runCheckDone() @@ -271,7 +271,7 @@ func TestMacosSetupAssistant(t *testing.T) { tm2, err = ds.SaveTeam(ctx, tm2) require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateProfile, &tm2.ID) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateProfile, &tm2.ID) require.NoError(t, err) runCheckDone() @@ -290,9 +290,9 @@ func TestMacosSetupAssistant(t *testing.T) { err = ds.DeleteTeam(ctx, tm2.ID) require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantHostsTransferred, &tm3.ID, "serial-2", "serial-4") + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantHostsTransferred, &tm3.ID, "serial-2", "serial-4") require.NoError(t, err) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantTeamDeleted, nil, "serial-5") // hosts[5] was in team 2 + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantTeamDeleted, nil, "serial-5") // hosts[5] was in team 2 require.NoError(t, err) runCheckDone() @@ -314,7 +314,7 @@ func TestMacosSetupAssistant(t *testing.T) { require.NoError(t, err) require.NotZero(t, noTmAsst.ID) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, nil) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantProfileChanged, nil) require.NoError(t, err) runCheckDone() @@ -331,7 +331,7 @@ func TestMacosSetupAssistant(t *testing.T) { reset := time.Now().Truncate(time.Second) time.Sleep(time.Second) - err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateAllProfiles, nil) + _, err = QueueMacosSetupAssistantJob(ctx, ds, logger, MacosSetupAssistantUpdateAllProfiles, nil) require.NoError(t, err) runCheckDone()