From 8bdad712d8b83c691b96c2b88c49c221fb1ececf Mon Sep 17 00:00:00 2001 From: Benjamin Edwards Date: Wed, 11 Aug 2021 10:40:56 -0400 Subject: [PATCH] add team_id filter to fleetctl & api (#1596) * add team_id filter to fleetctl via get hosts --team flag & api via api/v1/fleet/hosts and api/v1/fleet/labels/id/hosts * update tests & add changes file --- changes/issue-1325-filter-by-team_id | 2 + cmd/fleetctl/get.go | 11 +++- server/datastore/mysql/hosts.go | 9 ++++ server/datastore/mysql/hosts_test.go | 21 ++++++++ server/datastore/mysql/labels.go | 1 + server/datastore/mysql/labels_test.go | 77 +++++++++++++++++++++++++++ server/fleet/hosts.go | 2 + server/service/client_hosts.go | 4 +- server/service/transport.go | 10 ++++ 9 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 changes/issue-1325-filter-by-team_id diff --git a/changes/issue-1325-filter-by-team_id b/changes/issue-1325-filter-by-team_id new file mode 100644 index 0000000000..e8269c92f3 --- /dev/null +++ b/changes/issue-1325-filter-by-team_id @@ -0,0 +1,2 @@ +* add the `team_id` query parameter to `/api/v1/fleet/hosts` & `/api/v1/fleet/labels/{id}/hosts` which will filter the hosts by the specified team_id +* add a `--team` command line flag to fleetctl to allow filtering hosts by team i.e. `fleetctl get hosts --team=1` \ No newline at end of file diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 4a550d8655..aca3979434 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -561,6 +561,11 @@ func getHostsCommand() *cli.Command { Aliases: []string{"host", "h"}, Usage: "List information about one or more hosts", Flags: []cli.Flag{ + &cli.UintFlag{ + Name: "team", + Usage: "filter hosts by team_id", + Required: false, + }, jsonFlag(), yamlFlag(), configFlag(), @@ -576,7 +581,11 @@ func getHostsCommand() *cli.Command { identifier := c.Args().First() if identifier == "" { - hosts, err := client.GetHosts() + query := "" + if c.Uint("team") > 0 { + query = fmt.Sprintf("team_id=%d", c.Uint("team")) + } + hosts, err := client.GetHosts(query) if err != nil { return errors.Wrap(err, "could not list hosts") } diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index b494174f95..9a3c8c12ce 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -345,6 +345,7 @@ func (d *Datastore) ListHosts(filter fleet.TeamFilter, opt fleet.HostListOptions ) sql, params = filterHostsByStatus(sql, opt, params) + sql, params = filterHostsByTeam(sql, opt, params) sql, params = searchLike(sql, params, opt.MatchQuery, hostSearchColumns...) sql = appendListOptionsToSQL(sql, opt.ListOptions) @@ -357,6 +358,14 @@ func (d *Datastore) ListHosts(filter fleet.TeamFilter, opt fleet.HostListOptions return hosts, nil } +func filterHostsByTeam(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { + if opt.TeamFilter != nil { + sql += ` AND h.team_id = ?` + params = append(params, *opt.TeamFilter) + } + return sql, params +} + func filterHostsByStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { switch opt.StatusFilter { case "new": diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 2db68114c0..6a9201fbb4 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -389,10 +389,31 @@ func TestListHostsQuery(t *testing.T) { filter := fleet.TeamFilter{User: test.UserAdmin} + team1, err := ds.NewTeam(&fleet.Team{Name: "team1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(&fleet.Team{Name: "team2"}) + require.NoError(t, err) + + for _, host := range hosts { + require.NoError(t, ds.AddHostsToTeam(&team1.ID, []uint{host.ID})) + } + gotHosts, err := ds.ListHosts(filter, fleet.HostListOptions{}) require.Nil(t, err) assert.Equal(t, len(hosts), len(gotHosts)) + gotHosts, err = ds.ListHosts(filter, fleet.HostListOptions{TeamFilter: &team1.ID}) + require.NoError(t, err) + assert.Equal(t, len(hosts), len(gotHosts)) + + gotHosts, err = ds.ListHosts(filter, fleet.HostListOptions{TeamFilter: &team2.ID}) + require.NoError(t, err) + assert.Equal(t, 0, len(gotHosts)) + + gotHosts, err = ds.ListHosts(filter, fleet.HostListOptions{TeamFilter: nil}) + require.NoError(t, err) + assert.Equal(t, len(hosts), len(gotHosts)) + gotHosts, err = ds.ListHosts(filter, fleet.HostListOptions{ListOptions: fleet.ListOptions{MatchQuery: "00"}}) require.Nil(t, err) assert.Equal(t, 10, len(gotHosts)) diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index fd6716a333..cdb40d13ee 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -413,6 +413,7 @@ func (d *Datastore) ListHostsInLabel(filter fleet.TeamFilter, lid uint, opt flee params := []interface{}{lid} sql, params = filterHostsByStatus(sql, opt, params) + sql, params = filterHostsByTeam(sql, opt, params) sql, params = searchLike(sql, params, opt.MatchQuery, hostSearchColumns...) sql = appendListOptionsToSQL(sql, opt.ListOptions) diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index f097179c27..464a9d5082 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -388,6 +388,83 @@ func TestListHostsInLabelAndStatus(t *testing.T) { } } +func TestListHostsInLabelAndTeamFilter(t *testing.T) { + db := CreateMySQLDS(t) + defer db.Close() + + h1, err := db.NewHost(&fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: "1", + NodeKey: "1", + UUID: "1", + Hostname: "foo.local", + }) + require.Nil(t, err) + + lastSeenTime := time.Now().Add(-1000 * time.Hour) + h2, err := db.NewHost(&fleet.Host{ + DetailUpdatedAt: lastSeenTime, + LabelUpdatedAt: lastSeenTime, + SeenTime: lastSeenTime, + OsqueryHostID: "2", + NodeKey: "2", + UUID: "2", + Hostname: "bar.local", + }) + require.Nil(t, err) + + l1 := &fleet.LabelSpec{ + ID: 1, + Name: "label foo", + Query: "query1", + } + err = db.ApplyLabelSpecs([]*fleet.LabelSpec{l1}) + require.Nil(t, err) + + team1, err := db.NewTeam(&fleet.Team{Name: "team1"}) + require.NoError(t, err) + + team2, err := db.NewTeam(&fleet.Team{Name: "team2"}) + require.NoError(t, err) + + db.AddHostsToTeam(&team1.ID, []uint{h1.ID}) + + filter := fleet.TeamFilter{User: test.UserAdmin} + for _, h := range []*fleet.Host{h1, h2} { + err = db.RecordLabelQueryExecutions(h, map[uint]bool{l1.ID: true}, time.Now()) + assert.Nil(t, err) + } + + { + hosts, err := db.ListHostsInLabel(filter, l1.ID, fleet.HostListOptions{StatusFilter: fleet.StatusOnline}) + require.Nil(t, err) + require.Len(t, hosts, 1) + assert.Equal(t, "foo.local", hosts[0].Hostname) + } + + { + hosts, err := db.ListHostsInLabel(filter, l1.ID, fleet.HostListOptions{StatusFilter: fleet.StatusMIA}) + require.Nil(t, err) + require.Len(t, hosts, 1) + assert.Equal(t, "bar.local", hosts[0].Hostname) + } + + { + hosts, err := db.ListHostsInLabel(filter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID}) + require.Nil(t, err) + require.Len(t, hosts, 1) + assert.Equal(t, "foo.local", hosts[0].Hostname) + } + + { + hosts, err := db.ListHostsInLabel(filter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID}) + require.Nil(t, err) + require.Len(t, hosts, 0) + } +} + func TestBuiltInLabels(t *testing.T) { db := CreateMySQLDS(t) defer db.Close() diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 33ac52edb8..c249530c8b 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -109,6 +109,8 @@ type HostListOptions struct { AdditionalFilters []string // StatusFilter selects the online status of the hosts. StatusFilter HostStatus + // TeamFilter selects the hosts for specified team + TeamFilter *uint } type HostUser struct { diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index 9ba47afcc0..9e4c0e9fcf 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -11,8 +11,8 @@ import ( ) // GetHosts retrieves the list of all Hosts -func (c *Client) GetHosts() ([]HostResponse, error) { - response, err := c.AuthenticatedDo("GET", "/api/v1/fleet/hosts", "", nil) +func (c *Client) GetHosts(query string) ([]HostResponse, error) { + response, err := c.AuthenticatedDo("GET", "/api/v1/fleet/hosts", query, nil) if err != nil { return nil, errors.Wrap(err, "GET /api/v1/fleet/hosts") } diff --git a/server/service/transport.go b/server/service/transport.go index 834ee89b7b..38f07f61db 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -183,6 +183,16 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error) hopt.AdditionalFilters = strings.Split(additionalInfoFiltersString, ",") } + team_id := r.URL.Query().Get("team_id") + if team_id != "" { + id, err := strconv.Atoi(team_id) + if err != nil { + return hopt, err + } + tid := uint(id) + hopt.TeamFilter = &tid + } + return hopt, nil }