diff --git a/changes/issue-9643-add-fleetctl-get-hosts-mdm b/changes/issue-9643-add-fleetctl-get-hosts-mdm new file mode 100644 index 0000000000..f56164e891 --- /dev/null +++ b/changes/issue-9643-add-fleetctl-get-hosts-mdm @@ -0,0 +1,2 @@ +* Added the `--mdm` and `--mdm-pending` flags to the `fleetctl get hosts` command to list hosts enrolled in Fleet MDM and pending enrollment in Fleet MDM, respectively. +* Added support for the "enrolled" value for the `mdm_enrollment_status` filter and the new `mdm_name` filter for the "List hosts", "Count hosts" and "List hosts in label" endpoints. diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index d25f08681f..82ee784490 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -638,6 +638,14 @@ func getHostsCommand() *cli.Command { configFlag(), contextFlag(), debugFlag(), + &cli.BoolFlag{ + Name: "mdm", + Usage: "Filters hosts by hosts that have MDM turned on in Fleet and are connected to Fleet's MDM server.", + }, + &cli.BoolFlag{ + Name: "mdm-pending", + Usage: "Filters hosts by hosts ordered via Apple Business Manager (ABM). These will automatically enroll to Fleet and turn on MDM when they're unboxed.", + }, }, Action: func(c *cli.Context) error { client, err := clientFromCLI(c) @@ -653,6 +661,35 @@ func getHostsCommand() *cli.Command { if teamID := c.Uint("team"); teamID > 0 { query.Set("team_id", strconv.FormatUint(uint64(teamID), 10)) } + + if c.Bool("mdm") || c.Bool("mdm-pending") { + // print an error if MDM is not configured + appCfg, err := client.GetAppConfig() + if err != nil { + return err + } + if !appCfg.MDM.EnabledAndConfigured { + return errors.New("MDM features aren't turned on. Use `fleetctl generate mdm-apple` and then `fleet serve` with `mdm` configuration to turn on MDM features.") + } + + // --mdm and --mdm-pending are mutually exclusive, return an error if + // both are set (one returns the enrolled hosts, the other the pending + // to be enrolled, so it would always return an empty list). + if c.Bool("mdm") && c.Bool("mdm-pending") { + return errors.New("cannot use --mdm and --mdm-pending together") + } + + if c.Bool("mdm") { + // hosts enrolled (automatic or manual) in Fleet's MDM server + query.Set("mdm_name", fleet.WellKnownMDMFleet) + query.Set("mdm_enrollment_status", string(fleet.MDMEnrollStatusEnrolled)) + } + if c.Bool("mdm-pending") { + // hosts pending enrollment in Fleet's MDM server + query.Set("mdm_name", fleet.WellKnownMDMFleet) + query.Set("mdm_enrollment_status", string(fleet.MDMEnrollStatusPending)) + } + } queryStr := query.Encode() hosts, err := client.GetHosts(queryStr) diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 4cb8d84917..6fe0537e6d 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -244,8 +244,9 @@ func TestGetTeamsByName(t *testing.T) { func TestGetHosts(t *testing.T) { _, ds := runServerWithMockedDS(t) + var mdmEnabled bool ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { - return &fleet.AppConfig{}, nil + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: mdmEnabled}}, nil } // this func is called when no host is specified i.e. `fleetctl get hosts --json` @@ -356,6 +357,16 @@ func TestGetHosts(t *testing.T) { +------+------------+----------+-----------------+---------+ ` + assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "hosts"})) + + _, err := runAppNoChecks([]string{"get", "hosts", "--mdm"}) + require.Error(t, err) + assert.ErrorContains(t, err, "MDM features aren't turned on") + + _, err = runAppNoChecks([]string{"get", "hosts", "--mdm-pending"}) + require.Error(t, err) + assert.ErrorContains(t, err, "MDM features aren't turned on") + jsonPrettify := func(t *testing.T, v string) string { var i interface{} err := json.Unmarshal([]byte(v), &i) @@ -427,8 +438,113 @@ func TestGetHosts(t *testing.T) { } }) } +} - assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "hosts"})) +func TestGetHostsMDM(t *testing.T) { + _, ds := runServerWithMockedDS(t) + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } + + // this func is called when no host is specified i.e. `fleetctl get hosts --json` + ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { + additional := json.RawMessage(`{"query1": [{"col1": "val", "col2": 42}]}`) + hosts := []*fleet.Host{ + { + UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ + CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Time{}}, + UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Time{}}, + }, + HostSoftware: fleet.HostSoftware{}, + DetailUpdatedAt: time.Time{}, + LabelUpdatedAt: time.Time{}, + LastEnrolledAt: time.Time{}, + SeenTime: time.Time{}, + ComputerName: "test_host", + Hostname: "test_host", + Additional: &additional, + }, + { + UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ + CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Time{}}, + UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Time{}}, + }, + HostSoftware: fleet.HostSoftware{}, + DetailUpdatedAt: time.Time{}, + LabelUpdatedAt: time.Time{}, + LastEnrolledAt: time.Time{}, + SeenTime: time.Time{}, + ComputerName: "test_host2", + Hostname: "test_host2", + }, + } + return hosts, nil + } + + ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { + return nil + } + ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { + return make([]*fleet.Label, 0), nil + } + ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) { + return make([]*fleet.Pack, 0), nil + } + ds.ListHostBatteriesFunc = func(ctx context.Context, hid uint) (batteries []*fleet.HostBattery, err error) { + return nil, nil + } + ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + return nil, nil + } + + tests := []struct { + name string + args []string + goldenFile string + wantErr string + }{ + { + name: "get hosts --mdm --mdm-pending", + args: []string{"get", "hosts", "--mdm", "--mdm-pending"}, + wantErr: "cannot use --mdm and --mdm-pending together", + }, + { + name: "get hosts --mdm --json", + args: []string{"get", "hosts", "--mdm", "--json"}, + goldenFile: "expectedListHostsMDM.json", + }, + { + name: "get hosts --mdm-pending --yaml", + args: []string{"get", "hosts", "--mdm-pending", "--yaml"}, + goldenFile: "expectedListHostsYaml.yml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := runAppNoChecks(tt.args) + if tt.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + if tt.goldenFile != "" { + expected, err := ioutil.ReadFile(filepath.Join("testdata", tt.goldenFile)) + require.NoError(t, err) + if ext := filepath.Ext(tt.goldenFile); ext == ".json" { + // the output of --json is not a json array, but a list of + // newline-separated json objects. fix that for the assertion, + // turning it into a JSON array. + actual := "[" + strings.ReplaceAll(got.String(), "}\n{", "},{") + "]" + require.JSONEq(t, string(expected), actual) + } else { + require.YAMLEq(t, string(expected), got.String()) + } + } + }) + } } func TestGetConfig(t *testing.T) { diff --git a/cmd/fleetctl/testdata/expectedListHostsMDM.json b/cmd/fleetctl/testdata/expectedListHostsMDM.json new file mode 100644 index 0000000000..20e594984f --- /dev/null +++ b/cmd/fleetctl/testdata/expectedListHostsMDM.json @@ -0,0 +1,130 @@ +[ + { + "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", + "seen_time": "0001-01-01T00:00:00Z", + "software_updated_at": "0001-01-01T00:00:00Z", + "refetch_requested": false, + "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, + "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", + "seen_time": "0001-01-01T00:00:00Z", + "software_updated_at": "0001-01-01T00:00:00Z", + "refetch_requested": false, + "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, + "issues": { + "total_issues_count": 0, + "failing_policies_count": 0 + }, + "status": "offline", + "display_text": "test_host2" + } + } +] diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index ef18bfc1a6..4982bdf19c 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -1773,8 +1773,9 @@ the `software` table. | os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` | | device_mapping | boolean | query | Indicates whether `device_mapping` should be included for each host. See ["Get host's Google Chrome profiles](#get-hosts-google-chrome-profiles) for more information about this feature. | | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | -| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'pending', or 'unenrolled'. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | +| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | disable_failing_policies| boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | @@ -1785,7 +1786,7 @@ If `software_id` is specified, an additional top-level key `"software"` is retur If `mdm_id` is specified, an additional top-level key `"mobile_device_management_solution"` is returned with the information corresponding to the `mdm_id`. -If `mdm_id` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. +If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. If `munki_issue_id` is specified, an additional top-level key `"munki_issue"` is returned with the information corresponding to the `munki_issue_id`. @@ -1926,14 +1927,15 @@ Response payload with the `munki_issue_id` filter provided: | os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` | | label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `after`, `status`, `query` and `team_id`. | | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | -| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'pending', or 'unenrolled'. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | +| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | If `additional_info_filters` is not specified, no `additional` information will be returned. -If `mdm_id` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. +If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. #### Example @@ -3000,13 +3002,14 @@ requested by a web browser. | os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` | | os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` | | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | -| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'pending', or 'unenrolled'. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | +| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `status`, `query` and `team_id`. | -If `mdm_id` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. +If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. #### Example @@ -3385,9 +3388,14 @@ Returns a list of the hosts that belong to the specified label. | query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, and `ipv4`. | | team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. | | disable_failing_policies | boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | -| mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | +| mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | +| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'latest', 'pending', or 'failing'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | + +If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. + #### Example `GET /api/v1/fleet/labels/6/hosts&query=floobar` diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 8e5e770276..c87d493892 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -784,19 +784,25 @@ func filterHostsByMDM(sql string, opt fleet.HostListOptions, params []interface{ sql += ` AND hmdm.mdm_id = ?` params = append(params, *opt.MDMIDFilter) } + if opt.MDMNameFilter != nil { + sql += ` AND hmdm.name = ?` + params = append(params, *opt.MDMNameFilter) + } if opt.MDMEnrollmentStatusFilter != "" { switch opt.MDMEnrollmentStatusFilter { case fleet.MDMEnrollStatusAutomatic: sql += ` AND hmdm.enrolled = 1 AND hmdm.installed_from_dep = 1` case fleet.MDMEnrollStatusManual: sql += ` AND hmdm.enrolled = 1 AND hmdm.installed_from_dep = 0` + case fleet.MDMEnrollStatusEnrolled: + sql += ` AND hmdm.enrolled = 1` case fleet.MDMEnrollStatusPending: sql += ` AND hmdm.enrolled = 0 AND hmdm.installed_from_dep = 1` case fleet.MDMEnrollStatusUnenrolled: sql += ` AND hmdm.enrolled = 0 AND hmdm.installed_from_dep = 0` } } - if opt.MDMIDFilter != nil || opt.MDMEnrollmentStatusFilter != "" { + if opt.MDMNameFilter != nil || opt.MDMIDFilter != nil || opt.MDMEnrollmentStatusFilter != "" { sql += ` AND NOT COALESCE(hmdm.is_server, false) ` } return sql, params diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 5698ff196a..f57995bc0e 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -1093,7 +1093,7 @@ func testHostsListMDM(t *testing.T, ds *Datastore) { hostIDs = append(hostIDs, h.ID) } - // enrollment: pending + // enrollment: pending (with Fleet mdm) n, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, []godep.Device{ {SerialNumber: "532141num832", Model: "MacBook Pro", OS: "OSX", OpType: "added"}, }) @@ -1136,11 +1136,32 @@ func testHostsListMDM(t *testing.T, ds *Datastore) { hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusUnenrolled}, 1) assert.Equal(t, 1, len(hosts)) + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusEnrolled}, 3) // 2 auto, 1 manual + assert.Equal(t, 3, len(hosts)) + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusAutomatic, MDMIDFilter: &kandjiID}, 1) assert.Equal(t, 1, len(hosts)) + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusEnrolled, MDMIDFilter: &kandjiID}, 1) + assert.Equal(t, 1, len(hosts)) + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusPending}, 1) assert.Equal(t, 1, len(hosts)) + + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMSimpleMDM)}, 2) + assert.Equal(t, 2, len(hosts)) + + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMIDFilter: &simpleMDMID, MDMNameFilter: ptr.String(fleet.WellKnownMDMSimpleMDM), MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusEnrolled}, 1) + assert.Equal(t, 1, len(hosts)) + + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMKandji)}, 1) + assert.Equal(t, 1, len(hosts)) + + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMFleet), MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusPending}, 1) + assert.Equal(t, 1, len(hosts)) + + hosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMJamf)}, 0) + assert.Equal(t, 0, len(hosts)) } func testHostMDMSelect(t *testing.T, ds *Datastore) { diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index f4369e1bb3..4ec9fe8efd 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -326,6 +326,8 @@ func testLabelsListHostsInLabel(t *testing.T, db *Datastore) { listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMIDFilter: ptr.Uint(99)}, 0) listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMIDFilter: ptr.Uint(simpleMDMID)}, 2) listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMIDFilter: ptr.Uint(kandjiID)}, 1) + listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMSimpleMDM)}, 2) + listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMSimpleMDM), MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusEnrolled}, 1) } func listHostsInLabelCheckCount( diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 98bc614d99..36314cffd2 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -47,6 +47,7 @@ const ( MDMEnrollStatusAutomatic = MDMEnrollStatus("automatic") MDMEnrollStatusPending = MDMEnrollStatus("pending") MDMEnrollStatusUnenrolled = MDMEnrollStatus("unenrolled") + MDMEnrollStatusEnrolled = MDMEnrollStatus("enrolled") // combination of "manual" and "automatic" ) // MacOSSettingsStatus defines the possible statuses of the host's macOS settings, which is derived from the @@ -110,6 +111,9 @@ type HostListOptions struct { // MDMIDFilter filters the hosts by MDM ID. MDMIDFilter *uint + // MDMNameFilter filters the hosts by MDM solution name (e.g. one of the + // fleet.WellKnownMDM... constants). + MDMNameFilter *string // MDMEnrollmentStatusFilter filters the host by their MDM enrollment status. MDMEnrollmentStatusFilter MDMEnrollStatus // MunkiIssueIDFilter filters the hosts by munki issue ID. @@ -136,6 +140,7 @@ func (h HostListOptions) Empty() bool { h.OSVersionFilter == nil && h.DisableFailingPolicies == false && h.MDMIDFilter == nil && + h.MDMNameFilter == nil && h.MDMEnrollmentStatusFilter == "" && h.MunkiIssueIDFilter == nil && h.LowDiskSpaceFilter == nil diff --git a/server/service/transport.go b/server/service/transport.go index 53a0f6fc4d..6492f3ae6e 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -301,9 +301,14 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error) hopt.MDMIDFilter = &mid } + if mdmName := r.URL.Query().Get("mdm_name"); mdmName != "" { + hopt.MDMNameFilter = &mdmName + } + enrollmentStatus := r.URL.Query().Get("mdm_enrollment_status") switch fleet.MDMEnrollStatus(enrollmentStatus) { - case fleet.MDMEnrollStatusManual, fleet.MDMEnrollStatusAutomatic, fleet.MDMEnrollStatusPending, fleet.MDMEnrollStatusUnenrolled: + case fleet.MDMEnrollStatusManual, fleet.MDMEnrollStatusAutomatic, + fleet.MDMEnrollStatusPending, fleet.MDMEnrollStatusUnenrolled, fleet.MDMEnrollStatusEnrolled: hopt.MDMEnrollmentStatusFilter = fleet.MDMEnrollStatus(enrollmentStatus) case "": // No error when unset