Add options to populate users and labels on list hosts endpoint (#25621)

#22464

---------

Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
This commit is contained in:
Dante Catalfamo
2025-01-22 11:17:26 -05:00
committed by GitHub
co-authored by Lucas Manuel Rodriguez
parent 3e06ca21d9
commit 347c65b5c6
10 changed files with 139 additions and 1 deletions
@@ -0,0 +1 @@
- Added option to populate users and labels on list hosts endpoint
+9
View File
@@ -510,6 +510,15 @@ func loadHostUsersDB(ctx context.Context, db sqlx.QueryerContext, hostID uint) (
return users, nil
}
func (ds *Datastore) ListHostUsers(ctx context.Context, hostID uint) ([]fleet.HostUser, error) {
users, err := loadHostUsersDB(ctx, ds.reader(ctx), hostID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "loading host users")
}
return users, nil
}
// hostRefs are the tables referenced by hosts.
//
// Defined here for testing purposes.
+3
View File
@@ -252,6 +252,9 @@ type Datastore interface {
// the implementation for the exact list.
ListHostsLiteByIDs(ctx context.Context, ids []uint) ([]*Host, error)
// ListHostUsers returns a list of users that are currently on the host
ListHostUsers(ctx context.Context, hostID uint) ([]HostUser, error)
MarkHostsSeen(ctx context.Context, hostIDs []uint, t time.Time) error
SearchHosts(ctx context.Context, filter TeamFilter, query string, omit ...uint) ([]*Host, error)
// EnrolledHostIDs returns the full list of enrolled host IDs.
+1 -1
View File
@@ -13,7 +13,7 @@ type HostResponse struct {
Status HostStatus `json:"status" csv:"status"`
DisplayText string `json:"display_text" csv:"display_text"`
DisplayName string `json:"display_name" csv:"display_name"`
Labels []Label `json:"labels,omitempty" csv:"-"`
Labels []*Label `json:"labels,omitempty" csv:"-"`
Geolocation *GeoLocation `json:"geolocation,omitempty" csv:"-"`
CSVDeviceMapping string `json:"-" db:"-" csv:"device_mapping"`
}
+6
View File
@@ -207,6 +207,12 @@ type HostListOptions struct {
// PopulatePolicies adds the `Policies` array field to all Hosts returned.
PopulatePolicies bool
// PopulateUsers adds the `Users` array field to all Hosts returned
PopulateUsers bool
// PopulateLabels adds the `Labels` array field to all host responses returned
PopulateLabels bool
// VulnerabilityFilter filters the hosts by the presence of a vulnerability (CVE)
VulnerabilityFilter *string
+3
View File
@@ -263,6 +263,9 @@ type Service interface {
// ListHostsInLabel returns a slice of hosts in the label with the given ID.
ListHostsInLabel(ctx context.Context, lid uint, opt HostListOptions) ([]*Host, error)
// ListLabelsForHost returns a slice of labels for a given host
ListLabelsForHost(ctx context.Context, hostID uint) ([]*Label, error)
// BatchValidateLabels validates that each of the provided label names exists. The returned map
// is keyed by label name. Caller must ensure that appropirate authorization checks are
// performed prior to calling this method.
+12
View File
@@ -185,6 +185,8 @@ type ListHostsLiteByUUIDsFunc func(ctx context.Context, filter fleet.TeamFilter,
type ListHostsLiteByIDsFunc func(ctx context.Context, ids []uint) ([]*fleet.Host, error)
type ListHostUsersFunc func(ctx context.Context, hostID uint) ([]fleet.HostUser, error)
type MarkHostsSeenFunc func(ctx context.Context, hostIDs []uint, t time.Time) error
type SearchHostsFunc func(ctx context.Context, filter fleet.TeamFilter, query string, omit ...uint) ([]*fleet.Host, error)
@@ -1452,6 +1454,9 @@ type DataStore struct {
ListHostsLiteByIDsFunc ListHostsLiteByIDsFunc
ListHostsLiteByIDsFuncInvoked bool
ListHostUsersFunc ListHostUsersFunc
ListHostUsersFuncInvoked bool
MarkHostsSeenFunc MarkHostsSeenFunc
MarkHostsSeenFuncInvoked bool
@@ -3559,6 +3564,13 @@ func (s *DataStore) ListHostsLiteByIDs(ctx context.Context, ids []uint) ([]*flee
return s.ListHostsLiteByIDsFunc(ctx, ids)
}
func (s *DataStore) ListHostUsers(ctx context.Context, hostID uint) ([]fleet.HostUser, error) {
s.mu.Lock()
s.ListHostUsersFuncInvoked = true
s.mu.Unlock()
return s.ListHostUsersFunc(ctx, hostID)
}
func (s *DataStore) MarkHostsSeen(ctx context.Context, hostIDs []uint, t time.Time) error {
s.mu.Lock()
s.MarkHostsSeenFuncInvoked = true
+27
View File
@@ -143,7 +143,16 @@ func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi
for i, host := range hosts {
h := fleet.HostResponseForHost(ctx, svc, host)
hostResponses[i] = *h
if req.Opts.PopulateLabels {
labels, err := svc.ListLabelsForHost(ctx, h.ID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("failed to list labels for host %d", h.ID))
}
hostResponses[i].Labels = labels
}
}
return listHostsResponse{
Hosts: hostResponses,
Software: software,
@@ -228,6 +237,16 @@ func (svc *Service) ListHosts(ctx context.Context, opt fleet.HostListOptions) ([
}
}
if opt.PopulateUsers {
for _, host := range hosts {
hu, err := svc.ds.ListHostUsers(ctx, host.ID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("get users for host %d", host.ID))
}
host.Users = hu
}
}
return hosts, nil
}
@@ -1947,6 +1966,14 @@ func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
return hostsReportResponse{Columns: cols, Hosts: hostResps}, nil
}
func (svc *Service) ListLabelsForHost(ctx context.Context, hostID uint) ([]*fleet.Label, error) {
// require list hosts permission to view this information
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
return svc.ds.ListLabelsForHost(ctx, hostID)
}
type osVersionsRequest struct {
fleet.ListOptions
TeamID *uint `query:"team_id,optional"`
+55
View File
@@ -1950,6 +1950,61 @@ func (s *integrationTestSuite) TestListHosts() {
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", " local0 ")
require.Len(t, resp.Hosts, 1)
require.Contains(t, resp.Hosts[0].Hostname, "local0")
// Add users to hosts
users := []fleet.HostUser{
{
Uid: 1,
Username: "root",
Type: "local",
GroupName: "root",
Shell: "/bin/sh",
},
{
Uid: 1001,
Username: "username",
Type: "local",
GroupName: "usergroup",
Shell: "/bin/sh",
},
}
err = s.ds.SaveHostUsers(ctx, host0.ID, users)
require.NoError(t, err)
// Add labels to host
label1, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "First Label"})
require.NoError(t, err)
label2, err := s.ds.NewLabel(ctx, &fleet.Label{Name: "Second Label"})
require.NoError(t, err)
err = s.ds.AddLabelsToHost(ctx, host0.ID, []uint{label1.ID, label2.ID})
require.NoError(t, err)
// Without "populate_users" and "populate_labels" query params, no users or labels
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", "local0")
require.Len(t, resp.Hosts, 1)
require.Contains(t, resp.Hosts[0].Hostname, "local0")
require.Empty(t, resp.Hosts[0].Users)
require.Empty(t, resp.Hosts[0].Labels)
// With "populate_users" query param
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", "local0", "populate_users", "true")
require.Len(t, resp.Hosts, 1)
require.Contains(t, resp.Hosts[0].Hostname, "local0")
require.Len(t, resp.Hosts[0].Users, 2)
require.EqualValues(t, resp.Hosts[0].Users[0], users[0])
require.EqualValues(t, resp.Hosts[0].Users[1], users[1])
// With "populate_labels" query param
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", "local0", "populate_labels", "true")
require.Len(t, resp.Hosts, 1)
require.Contains(t, resp.Hosts[0].Hostname, "local0")
require.Len(t, resp.Hosts[0].Labels, 2)
require.Equal(t, label1.Name, resp.Hosts[0].Labels[0].Name)
require.Equal(t, label2.Name, resp.Hosts[0].Labels[1].Name)
}
func (s *integrationTestSuite) TestInvites() {
+22
View File
@@ -587,6 +587,28 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error)
hopt.PopulatePolicies = pp
}
populateUsers := r.URL.Query().Get("populate_users")
if populateUsers != "" {
pu, err := strconv.ParseBool(populateUsers)
if err != nil {
return hopt, ctxerr.Wrap(
r.Context(), badRequest(fmt.Sprintf("Invalid boolean parameter populate_users: %s", populateUsers)),
)
}
hopt.PopulateUsers = pu
}
populateLabels := r.URL.Query().Get("populate_labels")
if populateLabels != "" {
pl, err := strconv.ParseBool(populateLabels)
if err != nil {
return hopt, ctxerr.Wrap(
r.Context(), badRequest(fmt.Sprintf("Invalid boolean parameter populate_labels: %s", populateLabels)),
)
}
hopt.PopulateLabels = pl
}
// cannot combine software_id, software_version_id, and software_title_id
var softwareErrorLabel []string
if hopt.SoftwareIDFilter != nil {