Add mdm solution payload to GET /hosts response when filtering by mdm_id (#7198)

This commit is contained in:
Martin Angers 2022-08-15 12:57:25 -04:00 committed by GitHub
parent 11cefb6c61
commit b891e0d7f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 4 deletions

View File

@ -1736,9 +1736,12 @@ None.
| 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' or 'unenrolled'. |
### Get host's Google Chrome profiles
If `additional_info_filters` is not specified, no `additional` information will be returned.
If `software_id` is specified, an additional top-level key `"software"` is returned with the software object corresponding to the `software_id`. See [List all software](#list-all-software) response payload for details about this object.
If `mdm_id` is specified, an additional top-level key `"mobile_device_management_solution"` is returned with the aggregated statistics corresponding to the `mdm_id` (and, if provided, `team_id`). See [Get aggregated host's mobile device management (MDM) and Munki information](#get-aggregated-hosts-mobile-device-management-mdm-and-munki-information) response payload for details about this object. Note that the statistics are for the corresponding `mdm_id` (and `team_id` if provided) only, and do not take into account other potential filters such as `mdm_enrollment_status`.
#### Example
`GET /api/v1/fleet/hosts?page=0&per_page=100&order_key=hostname&query=2ce`
@ -2546,7 +2549,7 @@ Retrieves the aggregated host OS versions information.
| Name | Type | In | Description |
| --- | --- | --- | --- |
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. If not provided, all hosts are included. |
| platform | string | query | Filters the hosts to the specified platform |
| platform | string | query | Filters the hosts to the specified platform |
| operating_system_id | integer | query | Filters the hosts to the specified operating system id |
##### Default response

View File

@ -277,6 +277,7 @@ type Service interface {
MacadminsData(ctx context.Context, id uint) (*MacadminsData, error)
AggregatedMacadminsData(ctx context.Context, teamID *uint) (*AggregatedMacadminsData, error)
AggregatedMDMSolutions(ctx context.Context, teamID *uint, mdmID uint) (*AggregatedMDMSolutions, error)
// OSVersions returns a list of operating systems and associated host counts, which may be
// filtered using the following optional criteria: team id, platform, or operating system id

View File

@ -69,7 +69,11 @@ type listHostsRequest struct {
type listHostsResponse struct {
Hosts []HostResponse `json:"hosts"`
Software *fleet.Software `json:"software,omitempty"`
Err error `json:"error,omitempty"`
// MDMSolution is populated with the MDM solution corresponding to the mdm_id
// filter if one is provided with the request (and it exists in the
// database). It is nil otherwise and absent of the JSON response payload.
MDMSolution *fleet.AggregatedMDMSolutions `json:"mobile_device_management_solution,omitempty"`
Err error `json:"error,omitempty"`
}
func (r listHostsResponse) error() error { return r.Err }
@ -86,6 +90,15 @@ func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
}
}
var mdmSolution *fleet.AggregatedMDMSolutions
if req.Opts.MDMIDFilter != nil {
var err error
mdmSolution, err = svc.AggregatedMDMSolutions(ctx, req.Opts.TeamFilter, *req.Opts.MDMIDFilter)
if err != nil {
return listHostsResponse{Err: err}, nil
}
}
hosts, err := svc.ListHosts(ctx, req.Opts)
if err != nil {
return listHostsResponse{Err: err}, nil
@ -100,7 +113,39 @@ func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
hostResponses[i] = *h
}
return listHostsResponse{Hosts: hostResponses, Software: software}, nil
return listHostsResponse{Hosts: hostResponses, Software: software, MDMSolution: mdmSolution}, nil
}
func (svc *Service) AggregatedMDMSolutions(ctx context.Context, teamID *uint, mdmID uint) (*fleet.AggregatedMDMSolutions, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
return nil, err
}
if teamID != nil {
_, err := svc.ds.Team(ctx, *teamID)
if err != nil {
return nil, err
}
}
// it is expected that there will be relatively few MDM solutions. This
// returns the slice of all aggregated stats (one entry per mdm_id), and we
// then iterate to return only the one that was requested (the slice is
// stored as-is in a JSON field in the database).
sols, _, err := svc.ds.AggregatedMDMSolutions(ctx, teamID)
if err != nil {
return nil, err
}
for _, sol := range sols {
// don't take the address of the loop variable (although it could be ok
// here, but just bad practice)
sol := sol
if sol.ID == mdmID {
return &sol, nil
}
}
return nil, nil
}
func (svc *Service) ListHosts(ctx context.Context, opt fleet.HostListOptions) ([]*fleet.Host, error) {

View File

@ -957,10 +957,13 @@ func (s *integrationTestSuite) TestListHosts() {
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp)
require.Len(t, resp.Hosts, len(hosts))
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "per_page", "1")
require.Len(t, resp.Hosts, 1)
assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "order_key", "h.id", "after", fmt.Sprint(hosts[1].ID))
require.Len(t, resp.Hosts, len(hosts)-2)
@ -971,6 +974,7 @@ func (s *integrationTestSuite) TestListHosts() {
require.NoError(t, s.ds.UpdateHostSoftware(context.Background(), host.ID, software))
require.NoError(t, s.ds.LoadHostSoftware(context.Background(), host, false))
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "software_id", fmt.Sprint(host.Software[0].ID))
require.Len(t, resp.Hosts, 1)
assert.Equal(t, host.ID, resp.Hosts[0].ID)
@ -986,21 +990,29 @@ func (s *integrationTestSuite) TestListHosts() {
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{p.ID: ptr.Bool(false)}, time.Now(), false))
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "software_id", fmt.Sprint(host.Software[0].ID))
require.Len(t, resp.Hosts, 1)
assert.Equal(t, 1, resp.Hosts[0].HostIssues.FailingPoliciesCount)
assert.Equal(t, 1, resp.Hosts[0].HostIssues.TotalIssuesCount)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "software_id", fmt.Sprint(host.Software[0].ID), "disable_failing_policies", "true")
require.Len(t, resp.Hosts, 1)
assert.Equal(t, 0, resp.Hosts[0].HostIssues.FailingPoliciesCount)
assert.Equal(t, 0, resp.Hosts[0].HostIssues.TotalIssuesCount)
// filter by MDM criteria without any host having such information
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_id", fmt.Sprint(999))
require.Len(t, resp.Hosts, 0)
assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_enrollment_status", "manual")
require.Len(t, resp.Hosts, 0)
assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution)
// set MDM information on a host
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), host.ID, true, "https://simplemdm.com", false))
@ -1009,18 +1021,45 @@ func (s *integrationTestSuite) TestListHosts() {
return sqlx.GetContext(context.Background(), q, &mdmID,
`SELECT id FROM mobile_device_management_solutions WHERE name = ? AND server_url = ?`, fleet.WellKnownMDMSimpleMDM, "https://simplemdm.com")
})
// generate aggregated stats
require.NoError(t, s.ds.GenerateAggregatedMunkiAndMDM(context.Background()))
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_enrollment_status", "manual")
require.Len(t, resp.Hosts, 1)
assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_enrollment_status", "automatic")
require.Len(t, resp.Hosts, 0)
assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_enrollment_status", "unenrolled")
require.Len(t, resp.Hosts, 0)
assert.Nil(t, resp.Software)
assert.Nil(t, resp.MDMSolution)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_id", fmt.Sprint(mdmID))
require.Len(t, resp.Hosts, 1)
assert.Nil(t, resp.Software)
require.NotNil(t, resp.MDMSolution)
assert.Equal(t, mdmID, resp.MDMSolution.ID)
assert.Equal(t, 1, resp.MDMSolution.HostsCount)
assert.Equal(t, fleet.WellKnownMDMSimpleMDM, resp.MDMSolution.Name)
assert.Equal(t, "https://simplemdm.com", resp.MDMSolution.ServerURL)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "mdm_id", fmt.Sprint(mdmID), "mdm_enrollment_status", "manual")
require.Len(t, resp.Hosts, 1)
assert.Nil(t, resp.Software)
assert.NotNil(t, resp.MDMSolution)
assert.Equal(t, mdmID, resp.MDMSolution.ID)
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusInternalServerError, &resp, "mdm_enrollment_status", "invalid-status") // TODO: to be addressed by #4406
}