Filter query page API responses based on team membership (#850)

- Include only hosts that the user has access to in search targets API.
- Add parameter to specify whether `observer` hosts should be included.
- Generate counts based on which hosts user can access.
- Update API doc.
This commit is contained in:
Zach Wasserman 2021-05-24 21:34:08 -07:00 committed by GitHub
parent e33391e8d3
commit 15b81824f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 537 additions and 336 deletions

View File

@ -1361,11 +1361,11 @@ Returns a list of all enabled users
#### Parameters
| Name | Type | In | Description |
| --------------------- | ------ | ---- | --------------------------------------------------------------- |
| order_key | string | query | What to order results by. Can be any column in the users table. |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| query | string | query | Search query keywords. Searchable fields include `name` and `email`. |
| Name | Type | In | Description |
| --------------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------- |
| order_key | string | query | What to order results by. Can be any column in the users table. |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| query | string | query | Search query keywords. Searchable fields include `name` and `email`. |
#### Example
@ -1743,15 +1743,14 @@ Delete the specified user from Fleet.
#### Parameters
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------------------ |
| id | integer | path | **Required.** The user's id. |
| Name | Type | In | Description |
| ---- | ------- | ---- | ---------------------------- |
| id | integer | path | **Required.** The user's id. |
#### Example
`DELETE /api/v1/fleet/users/3`
##### Default response
`Status: 200`
@ -3750,14 +3749,17 @@ In Fleet, targets are used to run queries against specific hosts or groups of ho
The search targets endpoint returns two lists. The first list includes the possible target hosts in Fleet given the search query provided and the hosts already selected as targets. The second list includes the possible target labels in Fleet given the search query provided and the labels already selected as targets.
The returned lists are filtered based on the hosts the requesting user has access to.
`POST /api/v1/fleet/targets`
#### Parameters
| Name | Type | In | Description |
| -------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| query | string | body | The search query. Searchable items include a host's hostname or IPv4 address and labels. |
| selected | object | body | The targets already selected. The object includes a `hosts` property which contains a list of host IDs and a `labels` property which contains a list of label IDs. |
| Name | Type | In | Description |
| ---------------- | ------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| query | string | body | The search query. Searchable items include a host's hostname or IPv4 address and labels. |
| selected | object | body | The targets already selected. The object includes a `hosts` property which contains a list of host IDs and a `labels` property which contains a list of label IDs. |
| include_observer | boolean | body | Whether to include hosts that the user only has `observer` permission on. |
#### Example
@ -3771,7 +3773,8 @@ The search targets endpoint returns two lists. The first list includes the possi
"selected": {
"hosts": [],
"labels": [7]
}
},
"include_observer": true
}
```
@ -4181,12 +4184,12 @@ Modifies and/or creates the specified enroll secret(s).
#### Parameters
| Name | Type | In | Description |
| ---------- | ------- | ---- | ------------------------------------------------ |
| admin | boolean | body | **Required.** Whether or not the invited user will be granted admin privileges. |
| email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. |
| name | string | body | **Required.** The name of the invited user. |
| sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. |
| Name | Type | In | Description |
| ----------- | ------- | ---- | ----------------------------------------------------------------------------------------- |
| admin | boolean | body | **Required.** Whether or not the invited user will be granted admin privileges. |
| email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. |
| name | string | body | **Required.** The name of the invited user. |
| sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. |
#### Example
@ -4230,11 +4233,11 @@ Returns a list of the active invitations in Fleet.
#### Parameters
| Name | Type | In | Description |
| --------------------- | ------ | ---- | --------------------------------------------------------------- |
| order_key | string | query | What to order results by. Can be any column in the invites table. |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| query | string | query | Search query keywords. Searchable fields include `name` and `email`. |
| Name | Type | In | Description |
| --------------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------- |
| order_key | string | query | What to order results by. Can be any column in the invites table. |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| query | string | query | Search query keywords. Searchable fields include `name` and `email`. |
#### Example

View File

@ -495,31 +495,34 @@ func testSearchHosts(t *testing.T, ds kolide.Datastore) {
})
require.Nil(t, err)
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
// We once threw errors when the search query was empty. Verify that we
// don't error.
_, err = ds.SearchHosts("")
_, err = ds.SearchHosts(filter, "")
require.Nil(t, err)
hosts, err := ds.SearchHosts("foo")
hosts, err := ds.SearchHosts(filter, "foo")
assert.Nil(t, err)
assert.Len(t, hosts, 2)
host, err := ds.SearchHosts("foo", h3.ID)
host, err := ds.SearchHosts(filter, "foo", h3.ID)
require.Nil(t, err)
require.Len(t, host, 1)
assert.Equal(t, "foo.local", host[0].HostName)
host, err = ds.SearchHosts("foo", h3.ID, h2.ID)
host, err = ds.SearchHosts(filter, "foo", h3.ID, h2.ID)
require.Nil(t, err)
require.Len(t, host, 1)
assert.Equal(t, "foo.local", host[0].HostName)
host, err = ds.SearchHosts("abc")
host, err = ds.SearchHosts(filter, "abc")
require.Nil(t, err)
require.Len(t, host, 1)
assert.Equal(t, "abc-def-ghi", host[0].UUID)
none, err := ds.SearchHosts("xxx")
none, err := ds.SearchHosts(filter, "xxx")
assert.Nil(t, err)
assert.Len(t, none, 0)
@ -528,26 +531,29 @@ func testSearchHosts(t *testing.T, ds kolide.Datastore) {
err = ds.SaveHost(h2)
require.Nil(t, err)
hits, err := ds.SearchHosts("99.100.101")
hits, err := ds.SearchHosts(filter, "99.100.101")
require.Nil(t, err)
require.Equal(t, 1, len(hits))
hits, err = ds.SearchHosts("99.100.111")
hits, err = ds.SearchHosts(filter, "99.100.111")
require.Nil(t, err)
assert.Equal(t, 0, len(hits))
h3.PrimaryIP = "99.100.101.104"
err = ds.SaveHost(h3)
require.Nil(t, err)
hits, err = ds.SearchHosts("99.100.101")
hits, err = ds.SearchHosts(filter, "99.100.101")
require.Nil(t, err)
assert.Equal(t, 2, len(hits))
hits, err = ds.SearchHosts("99.100.101", h3.ID)
hits, err = ds.SearchHosts(filter, "99.100.101", h3.ID)
require.Nil(t, err)
assert.Equal(t, 1, len(hits))
}
func testSearchHostsLimit(t *testing.T, ds kolide.Datastore) {
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
for i := 0; i < 15; i++ {
_, err := ds.NewHost(&kolide.Host{
DetailUpdateTime: time.Now(),
@ -561,7 +567,7 @@ func testSearchHostsLimit(t *testing.T, ds kolide.Datastore) {
require.Nil(t, err)
}
hosts, err := ds.SearchHosts("foo")
hosts, err := ds.SearchHosts(filter, "foo")
require.Nil(t, err)
assert.Len(t, hosts, 10)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/fleetdm/fleet/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)
func testLabels(t *testing.T, db kolide.Datastore) {
@ -239,23 +240,26 @@ func testSearchLabels(t *testing.T, db kolide.Datastore) {
l3, err := db.Label(specs[2].ID)
require.Nil(t, err)
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
// We once threw errors when the search query was empty. Verify that we
// don't error.
labels, err := db.SearchLabels("")
labels, err := db.SearchLabels(filter, "")
require.Nil(t, err)
assert.Contains(t, labels, *all)
labels, err = db.SearchLabels("foo")
labels, err = db.SearchLabels(filter, "foo")
require.Nil(t, err)
assert.Len(t, labels, 3)
assert.Contains(t, labels, *all)
labels, err = db.SearchLabels("foo", all.ID, l3.ID)
labels, err = db.SearchLabels(filter, "foo", all.ID, l3.ID)
require.Nil(t, err)
assert.Len(t, labels, 1)
assert.Equal(t, "foo", labels[0].Name)
labels, err = db.SearchLabels("xxx")
labels, err = db.SearchLabels(filter, "xxx")
require.Nil(t, err)
assert.Len(t, labels, 1)
assert.Contains(t, labels, *all)
@ -281,7 +285,10 @@ func testSearchLabelsLimit(t *testing.T, db kolide.Datastore) {
require.Nil(t, err)
}
labels, err := db.SearchLabels("foo")
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
labels, err := db.SearchLabels(filter, "foo")
require.Nil(t, err)
assert.Len(t, labels, 11)
}
@ -350,7 +357,10 @@ func testListHostsInLabel(t *testing.T, db kolide.Datastore) {
func testBuiltInLabels(t *testing.T, db kolide.Datastore) {
require.Nil(t, db.MigrateData())
hits, err := db.SearchLabels("macOS")
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
hits, err := db.SearchLabels(filter, "macOS")
require.Nil(t, err)
// Should get Mac OS X and All Hosts
assert.Equal(t, 2, len(hits))

View File

@ -10,6 +10,7 @@ import (
"github.com/fleetdm/fleet/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)
func testCountHostsInTargets(t *testing.T, ds kolide.Datastore) {
@ -17,6 +18,9 @@ func testCountHostsInTargets(t *testing.T, ds kolide.Datastore) {
t.Skip("inmem is being deprecated, test skipped")
}
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
mockClock := clock.NewMockClock()
hostCount := 0
@ -67,42 +71,42 @@ func testCountHostsInTargets(t *testing.T, ds kolide.Datastore) {
assert.Nil(t, err)
}
metrics, err := ds.CountHostsInTargets(nil, []uint{l1.ID, l2.ID}, mockClock.Now())
metrics, err := ds.CountHostsInTargets(filter, nil, []uint{l1.ID, l2.ID}, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(6), metrics.TotalHosts)
assert.Equal(t, uint(2), metrics.OfflineHosts)
assert.Equal(t, uint(3), metrics.OnlineHosts)
assert.Equal(t, uint(1), metrics.MissingInActionHosts)
metrics, err = ds.CountHostsInTargets([]uint{h1.ID, h2.ID}, []uint{l1.ID, l2.ID}, mockClock.Now())
metrics, err = ds.CountHostsInTargets(filter, []uint{h1.ID, h2.ID}, []uint{l1.ID, l2.ID}, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(6), metrics.TotalHosts)
assert.Equal(t, uint(2), metrics.OfflineHosts)
assert.Equal(t, uint(3), metrics.OnlineHosts)
assert.Equal(t, uint(1), metrics.MissingInActionHosts)
metrics, err = ds.CountHostsInTargets([]uint{h1.ID, h2.ID}, nil, mockClock.Now())
metrics, err = ds.CountHostsInTargets(filter, []uint{h1.ID, h2.ID}, nil, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(2), metrics.TotalHosts)
assert.Equal(t, uint(1), metrics.OnlineHosts)
assert.Equal(t, uint(1), metrics.OfflineHosts)
assert.Equal(t, uint(0), metrics.MissingInActionHosts)
metrics, err = ds.CountHostsInTargets([]uint{h1.ID}, []uint{l2.ID}, mockClock.Now())
metrics, err = ds.CountHostsInTargets(filter, []uint{h1.ID}, []uint{l2.ID}, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(4), metrics.TotalHosts)
assert.Equal(t, uint(3), metrics.OnlineHosts)
assert.Equal(t, uint(1), metrics.OfflineHosts)
assert.Equal(t, uint(0), metrics.MissingInActionHosts)
metrics, err = ds.CountHostsInTargets(nil, nil, mockClock.Now())
metrics, err = ds.CountHostsInTargets(filter, nil, nil, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(0), metrics.TotalHosts)
assert.Equal(t, uint(0), metrics.OnlineHosts)
assert.Equal(t, uint(0), metrics.OfflineHosts)
assert.Equal(t, uint(0), metrics.MissingInActionHosts)
metrics, err = ds.CountHostsInTargets([]uint{}, []uint{}, mockClock.Now())
metrics, err = ds.CountHostsInTargets(filter, []uint{}, []uint{}, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(0), metrics.TotalHosts)
assert.Equal(t, uint(0), metrics.OnlineHosts)
@ -111,7 +115,7 @@ func testCountHostsInTargets(t *testing.T, ds kolide.Datastore) {
// Advance clock so all hosts are offline
mockClock.AddTime(2 * time.Minute)
metrics, err = ds.CountHostsInTargets(nil, []uint{l1.ID, l2.ID}, mockClock.Now())
metrics, err = ds.CountHostsInTargets(filter, nil, []uint{l1.ID, l2.ID}, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, uint(6), metrics.TotalHosts)
assert.Equal(t, uint(0), metrics.OnlineHosts)
@ -132,6 +136,9 @@ func testHostStatus(t *testing.T, ds kolide.Datastore) {
h, err := ds.EnrollHost("1", "key1", "default", 0)
require.Nil(t, err)
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
// Make host no longer appear new
mockClock.AddTime(36 * time.Hour)
@ -174,7 +181,7 @@ func testHostStatus(t *testing.T, ds kolide.Datastore) {
require.Nil(t, ds.MarkHostSeen(h, tt.seenTime))
// Verify status
metrics, err := ds.CountHostsInTargets([]uint{h.ID}, []uint{}, mockClock.Now())
metrics, err := ds.CountHostsInTargets(filter, []uint{h.ID}, []uint{}, mockClock.Now())
require.Nil(t, err)
assert.Equal(t, tt.metrics, metrics)
})
@ -186,6 +193,9 @@ func testHostIDsInTargets(t *testing.T, ds kolide.Datastore) {
t.Skip("inmem is being deprecated, test skipped")
}
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
filter := kolide.TeamFilter{User: user}
hostCount := 0
initHost := func() *kolide.Host {
hostCount += 1
@ -230,31 +240,31 @@ func testHostIDsInTargets(t *testing.T, ds kolide.Datastore) {
assert.Nil(t, err)
}
ids, err := ds.HostIDsInTargets(nil, []uint{l1.ID, l2.ID})
ids, err := ds.HostIDsInTargets(filter, nil, []uint{l1.ID, l2.ID})
require.Nil(t, err)
assert.Equal(t, []uint{1, 2, 3, 4, 5, 6}, ids)
ids, err = ds.HostIDsInTargets([]uint{h1.ID}, nil)
ids, err = ds.HostIDsInTargets(filter, []uint{h1.ID}, nil)
require.Nil(t, err)
assert.Equal(t, []uint{1}, ids)
ids, err = ds.HostIDsInTargets([]uint{h1.ID}, []uint{l1.ID})
ids, err = ds.HostIDsInTargets(filter, []uint{h1.ID}, []uint{l1.ID})
require.Nil(t, err)
assert.Equal(t, []uint{1, 2, 3, 6}, ids)
ids, err = ds.HostIDsInTargets([]uint{4}, []uint{l1.ID})
ids, err = ds.HostIDsInTargets(filter, []uint{4}, []uint{l1.ID})
require.Nil(t, err)
assert.Equal(t, []uint{1, 2, 3, 4, 6}, ids)
ids, err = ds.HostIDsInTargets([]uint{4}, []uint{l2.ID})
ids, err = ds.HostIDsInTargets(filter, []uint{4}, []uint{l2.ID})
require.Nil(t, err)
assert.Equal(t, []uint{3, 4, 5}, ids)
ids, err = ds.HostIDsInTargets([]uint{}, []uint{l2.ID})
ids, err = ds.HostIDsInTargets(filter, []uint{}, []uint{l2.ID})
require.Nil(t, err)
assert.Equal(t, []uint{3, 4, 5}, ids)
ids, err = ds.HostIDsInTargets([]uint{1, 6}, []uint{l2.ID})
ids, err = ds.HostIDsInTargets(filter, []uint{1, 6}, []uint{l2.ID})
require.Nil(t, err)
assert.Equal(t, []uint{1, 3, 4, 5, 6}, ids)
}

View File

@ -214,7 +214,7 @@ func (d *Datastore) MarkHostSeen(host *kolide.Host, t time.Time) error {
return nil
}
func (d *Datastore) SearchHosts(query string, omit ...uint) ([]*kolide.Host, error) {
func (d *Datastore) SearchHosts(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error) {
omitLookup := map[uint]bool{}
for _, o := range omit {
omitLookup[o] = true

View File

@ -151,7 +151,7 @@ func (d *Datastore) ListLabels(opt kolide.ListOptions) ([]*kolide.Label, error)
return labels, nil
}
func (d *Datastore) SearchLabels(query string, omit ...uint) ([]kolide.Label, error) {
func (d *Datastore) SearchLabels(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error) {
omitLookup := map[uint]bool{}
for _, o := range omit {
omitLookup[o] = true

View File

@ -6,7 +6,7 @@ import (
"github.com/fleetdm/fleet/server/kolide"
)
func (d *Datastore) CountHostsInTargets(hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
func (d *Datastore) CountHostsInTargets(filter kolide.TeamFilter, hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
// noop
return kolide.TargetMetrics{}, nil
}

View File

@ -602,24 +602,24 @@ func (d *Datastore) MarkHostsSeen(hostIDs []uint, t time.Time) error {
return nil
}
func (d *Datastore) searchHostsWithOmits(query string, omit ...uint) ([]*kolide.Host, error) {
func (d *Datastore) searchHostsWithOmits(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error) {
hostQuery := transformQuery(query)
ipQuery := `"` + query + `"`
sqlStatement :=
`
SELECT DISTINCT *
FROM hosts
WHERE
(
MATCH (host_name, uuid) AGAINST (? IN BOOLEAN MODE)
OR MATCH (primary_ip, primary_mac) AGAINST (? IN BOOLEAN MODE)
)
AND id NOT IN (?)
LIMIT 10
`
sql := fmt.Sprintf(`
SELECT DISTINCT *
FROM hosts
WHERE
(
MATCH (host_name, uuid) AGAINST (? IN BOOLEAN MODE)
OR MATCH (primary_ip, primary_mac) AGAINST (? IN BOOLEAN MODE)
)
AND id NOT IN (?) AND %s
LIMIT 10
`, d.whereFilterHostsByTeams(filter, "hosts"),
)
sql, args, err := sqlx.In(sqlStatement, hostQuery, ipQuery, omit)
sql, args, err := sqlx.In(sql, hostQuery, ipQuery, omit)
if err != nil {
return nil, errors.Wrap(err, "searching hosts")
}
@ -635,13 +635,14 @@ func (d *Datastore) searchHostsWithOmits(query string, omit ...uint) ([]*kolide.
return hosts, nil
}
func (d *Datastore) searchHostsDefault(omit ...uint) ([]*kolide.Host, error) {
sqlStatement := `
SELECT * FROM hosts
WHERE id NOT in (?)
ORDER BY seen_time DESC
LIMIT 5
`
func (d *Datastore) searchHostsDefault(filter kolide.TeamFilter, omit ...uint) ([]*kolide.Host, error) {
sql := fmt.Sprintf(`
SELECT * FROM hosts
WHERE id NOT in (?) AND %s
ORDER BY seen_time DESC
LIMIT 5
`, d.whereFilterHostsByTeams(filter, "hosts"),
)
var in interface{}
{
@ -654,7 +655,7 @@ func (d *Datastore) searchHostsDefault(omit ...uint) ([]*kolide.Host, error) {
}
var hosts []*kolide.Host
sql, args, err := sqlx.In(sqlStatement, in)
sql, args, err := sqlx.In(sql, in)
if err != nil {
return nil, errors.Wrap(err, "searching default hosts")
}
@ -668,32 +669,32 @@ func (d *Datastore) searchHostsDefault(omit ...uint) ([]*kolide.Host, error) {
// SearchHosts find hosts by query containing an IP address, a host name or UUID.
// Optionally pass a list of IDs to omit from the search
func (d *Datastore) SearchHosts(query string, omit ...uint) ([]*kolide.Host, error) {
func (d *Datastore) SearchHosts(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error) {
hostQuery := transformQuery(query)
if !queryMinLength(hostQuery) {
return d.searchHostsDefault(omit...)
return d.searchHostsDefault(filter, omit...)
}
if len(omit) > 0 {
return d.searchHostsWithOmits(query, omit...)
return d.searchHostsWithOmits(filter, query, omit...)
}
// Needs quotes to avoid each . marking a word boundary
ipQuery := `"` + query + `"`
sqlStatement :=
`
SELECT DISTINCT *
FROM hosts
WHERE
(
MATCH (host_name, uuid) AGAINST (? IN BOOLEAN MODE)
OR MATCH (primary_ip, primary_mac) AGAINST (? IN BOOLEAN MODE)
)
LIMIT 10
`
hosts := []*kolide.Host{}
sql := fmt.Sprintf(`
SELECT DISTINCT *
FROM hosts
WHERE
(
MATCH (host_name, uuid) AGAINST (? IN BOOLEAN MODE)
OR MATCH (primary_ip, primary_mac) AGAINST (? IN BOOLEAN MODE)
) AND %s
LIMIT 10
`, d.whereFilterHostsByTeams(filter, "hosts"),
)
if err := d.db.Select(&hosts, sqlStatement, hostQuery, ipQuery); err != nil {
hosts := []*kolide.Host{}
if err := d.db.Select(&hosts, sql, hostQuery, ipQuery); err != nil {
return nil, errors.Wrap(err, "searching hosts")
}

View File

@ -2,6 +2,7 @@ package mysql
import (
"database/sql"
"fmt"
"strings"
"time"
@ -437,19 +438,24 @@ func (d *Datastore) ListUniqueHostsInLabels(labels []uint) ([]kolide.Host, error
}
func (d *Datastore) searchLabelsWithOmits(query string, omit ...uint) ([]kolide.Label, error) {
func (d *Datastore) searchLabelsWithOmits(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error) {
transformedQuery := transformQuery(query)
sqlStatement := `
SELECT *, (SELECT COUNT(1) FROM label_membership WHERE label_id = id) AS host_count
FROM labels
WHERE (
MATCH(name) AGAINST(? IN BOOLEAN MODE)
)
AND id NOT IN (?)
ORDER BY label_type DESC, id ASC
LIMIT 10
`
sqlStatement := fmt.Sprintf(`
SELECT *,
(SELECT COUNT(1)
FROM label_membership lm JOIN hosts h ON (lm.host_id = h.id)
WHERE label_id = l.id AND %s
) AS host_count
FROM labels l
WHERE (
MATCH(name) AGAINST(? IN BOOLEAN MODE)
)
AND id NOT IN (?)
ORDER BY label_type DESC, id ASC
LIMIT 10
`, d.whereFilterHostsByTeams(filter, "h"),
)
sql, args, err := sqlx.In(sqlStatement, transformedQuery, omit)
if err != nil {
@ -464,7 +470,7 @@ func (d *Datastore) searchLabelsWithOmits(query string, omit ...uint) ([]kolide.
return nil, errors.Wrap(err, "selecting labels with omits")
}
matches, err = d.addAllHostsLabelToList(matches, omit...)
matches, err = d.addAllHostsLabelToList(filter, matches, omit...)
if err != nil {
return nil, errors.Wrap(err, "adding all hosts label to matches")
}
@ -475,20 +481,24 @@ func (d *Datastore) searchLabelsWithOmits(query string, omit ...uint) ([]kolide.
// When we search labels, we always want to make sure that the All Hosts label
// is included in the results set. Sometimes it already is and we don't need to
// add it, sometimes it's not so we explicitly add it.
func (d *Datastore) addAllHostsLabelToList(labels []kolide.Label, omit ...uint) ([]kolide.Label, error) {
sqlStatement := `
SELECT *, (SELECT COUNT(1) FROM label_membership WHERE label_id = id) AS host_count
FROM labels
WHERE
label_type=?
AND name = 'All Hosts'
LIMIT 1
`
func (d *Datastore) addAllHostsLabelToList(filter kolide.TeamFilter, labels []kolide.Label, omit ...uint) ([]kolide.Label, error) {
sql := fmt.Sprintf(`
SELECT *,
(SELECT COUNT(1)
FROM label_membership lm JOIN hosts h ON (lm.host_id = h.id)
WHERE label_id = l.id AND %s
) AS host_count
FROM labels l
WHERE
label_type=?
AND name = 'All Hosts'
LIMIT 1
`, d.whereFilterHostsByTeams(filter, "h"),
)
var allHosts kolide.Label
err := d.db.Get(&allHosts, sqlStatement, kolide.LabelTypeBuiltIn)
if err != nil {
return nil, errors.Wrap(err, "getting all hosts label")
if err := d.db.Get(&allHosts, sql, kolide.LabelTypeBuiltIn); err != nil {
return nil, errors.Wrap(err, "get all hosts label")
}
for _, omission := range omit {
@ -506,15 +516,20 @@ func (d *Datastore) addAllHostsLabelToList(labels []kolide.Label, omit ...uint)
return append(labels, allHosts), nil
}
func (d *Datastore) searchLabelsDefault(omit ...uint) ([]kolide.Label, error) {
sqlStatement := `
SELECT *, (SELECT COUNT(1) FROM label_membership WHERE label_id = id) AS host_count
FROM labels
WHERE id NOT IN (?)
GROUP BY id
ORDER BY label_type DESC, id ASC
LIMIT 7
`
func (d *Datastore) searchLabelsDefault(filter kolide.TeamFilter, omit ...uint) ([]kolide.Label, error) {
sql := fmt.Sprintf(`
SELECT *,
(SELECT COUNT(1)
FROM label_membership lm JOIN hosts h ON (lm.host_id = h.id)
WHERE label_id = l.id AND %s
) AS host_count
FROM labels l
WHERE id NOT IN (?)
GROUP BY id
ORDER BY label_type DESC, id ASC
LIMIT 7
`, d.whereFilterHostsByTeams(filter, "h"),
)
var in interface{}
{
@ -527,17 +542,16 @@ func (d *Datastore) searchLabelsDefault(omit ...uint) ([]kolide.Label, error) {
}
var labels []kolide.Label
sql, args, err := sqlx.In(sqlStatement, in)
sql, args, err := sqlx.In(sql, in)
if err != nil {
return nil, errors.Wrap(err, "searching default labels")
}
sql = d.db.Rebind(sql)
err = d.db.Select(&labels, sql, args...)
if err != nil {
if err := d.db.Select(&labels, sql, args...); err != nil {
return nil, errors.Wrap(err, "searching default labels rebound")
}
labels, err = d.addAllHostsLabelToList(labels, omit...)
labels, err = d.addAllHostsLabelToList(filter, labels, omit...)
if err != nil {
return nil, errors.Wrap(err, "getting all host label")
}
@ -546,35 +560,40 @@ func (d *Datastore) searchLabelsDefault(omit ...uint) ([]kolide.Label, error) {
}
// SearchLabels performs wildcard searches on kolide.Label name
func (d *Datastore) SearchLabels(query string, omit ...uint) ([]kolide.Label, error) {
func (d *Datastore) SearchLabels(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error) {
transformedQuery := transformQuery(query)
if !queryMinLength(transformedQuery) {
return d.searchLabelsDefault(omit...)
return d.searchLabelsDefault(filter, omit...)
}
if len(omit) > 0 {
return d.searchLabelsWithOmits(query, omit...)
return d.searchLabelsWithOmits(filter, query, omit...)
}
// Ordering first by label_type ensures that built-in labels come
// first. We will probably need to make a custom ordering function here
// if additional label types are added. Ordering next by ID ensures
// that the order is always consistent.
sqlStatement := `
SELECT *, (SELECT COUNT(1) FROM label_membership WHERE label_id = id) AS host_count
FROM labels
WHERE (
MATCH(name) AGAINST(? IN BOOLEAN MODE)
)
ORDER BY label_type DESC, id ASC
LIMIT 10
`
sql := fmt.Sprintf(`
SELECT *,
(SELECT COUNT(1)
FROM label_membership lm JOIN hosts h ON (lm.host_id = h.id)
WHERE label_id = l.id AND %s
) AS host_count
FROM labels l
WHERE (
MATCH(name) AGAINST(? IN BOOLEAN MODE)
)
ORDER BY label_type DESC, id ASC
LIMIT 10
`, d.whereFilterHostsByTeams(filter, "h"),
)
matches := []kolide.Label{}
err := d.db.Select(&matches, sqlStatement, transformedQuery)
if err != nil {
if err := d.db.Select(&matches, sql, transformedQuery); err != nil {
return nil, errors.Wrap(err, "selecting labels for search")
}
matches, err = d.addAllHostsLabelToList(matches, omit...)
matches, err := d.addAllHostsLabelToList(filter, matches, omit...)
if err != nil {
return nil, errors.Wrap(err, "adding all hosts label to matches")
}

View File

@ -9,6 +9,7 @@ import (
"io/ioutil"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@ -20,6 +21,7 @@ import (
"github.com/fleetdm/fleet/server/datastore/mysql/migrations/tables"
"github.com/fleetdm/fleet/server/kolide"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
@ -337,6 +339,53 @@ func appendListOptionsToSQL(sql string, opts kolide.ListOptions) string {
return sql
}
// whereFilterHostsByTeams returns the appropriate condition to use in the WHERE
// clause to render only the appropriate teams.
//
// filter provides the filtering parameters that should be used. hostKey is the
// name/alias of the hosts table to use in generating the SQL.
func (d *Datastore) whereFilterHostsByTeams(filter kolide.TeamFilter, hostKey string) string {
if filter.User == nil {
// This is likely unintentional, however we would like to return no
// results rather than panicking or returning some other error. At least
// log.
level.Info(d.logger).Log("err", "team filter missing user")
return "FALSE"
}
switch filter.User.GlobalRole.String {
case kolide.RoleAdmin, kolide.RoleMaintainer:
return "TRUE"
case kolide.RoleObserver:
if filter.IncludeObserver {
return "TRUE"
} else {
return "FALSE"
}
default:
// Fall through to specific teams
}
// Collect matching teams
var idStrs []string
for _, team := range filter.User.Teams {
if team.Role == kolide.RoleAdmin || team.Role == kolide.RoleMaintainer ||
(team.Role == kolide.RoleObserver && filter.IncludeObserver) {
idStrs = append(idStrs, strconv.Itoa(int(team.ID)))
}
}
if len(idStrs) == 0 {
// User has no global role and no teams allowed by includeObserver.
return "FALSE"
}
return fmt.Sprintf("%s.team_id IN (%s)", hostKey, strings.Join(idStrs, ","))
}
// registerTLS adds client certificate configuration to the mysql connection.
func registerTLS(config config.MysqlConfig) error {
rootCertPool := x509.NewCertPool()

View File

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)
func TestSanitizeColumn(t *testing.T) {
@ -282,3 +283,146 @@ func TestAppendListOptionsToSQL(t *testing.T) {
}
}
func TestWhereFilterHostsByTeams(t *testing.T) {
t.Parallel()
testCases := []struct {
filter kolide.TeamFilter
expected string
}{
// No teams or global role
{
filter: kolide.TeamFilter{
User: &kolide.User{},
},
expected: "FALSE",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{Teams: []kolide.UserTeam{}},
},
expected: "FALSE",
},
// Global role
{
filter: kolide.TeamFilter{
User: &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)},
},
expected: "TRUE",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{GlobalRole: null.StringFrom(kolide.RoleMaintainer)},
},
expected: "TRUE",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{GlobalRole: null.StringFrom(kolide.RoleObserver)},
},
expected: "FALSE",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{GlobalRole: null.StringFrom(kolide.RoleObserver)},
IncludeObserver: true,
},
expected: "TRUE",
},
// Team roles
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
},
},
},
expected: "FALSE",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
},
},
IncludeObserver: true,
},
expected: "hosts.team_id IN (1)",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 2}},
},
},
},
expected: "FALSE",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
{Role: kolide.RoleMaintainer, Team: kolide.Team{ID: 2}},
},
},
},
expected: "hosts.team_id IN (2)",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
{Role: kolide.RoleMaintainer, Team: kolide.Team{ID: 2}},
},
},
IncludeObserver: true,
},
expected: "hosts.team_id IN (1,2)",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
{Role: kolide.RoleMaintainer, Team: kolide.Team{ID: 2}},
// Invalid role should be ignored
{Role: "bad", Team: kolide.Team{ID: 37}},
},
},
},
expected: "hosts.team_id IN (2)",
},
{
filter: kolide.TeamFilter{
User: &kolide.User{
Teams: []kolide.UserTeam{
{Role: kolide.RoleObserver, Team: kolide.Team{ID: 1}},
{Role: kolide.RoleMaintainer, Team: kolide.Team{ID: 2}},
{Role: kolide.RoleAdmin, Team: kolide.Team{ID: 3}},
// Invalid role should be ignored
},
},
},
expected: "hosts.team_id IN (2,3)",
},
}
for _, tt := range testCases {
tt := tt
t.Run("", func(t *testing.T) {
t.Parallel()
ds := &Datastore{logger: log.NewNopLogger()}
sql := ds.whereFilterHostsByTeams(tt.filter, "hosts")
assert.Equal(t, tt.expected, sql)
})
}
}

View File

@ -9,7 +9,7 @@ import (
"github.com/pkg/errors"
)
func (d *Datastore) CountHostsInTargets(hostIDs []uint, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
func (d *Datastore) CountHostsInTargets(filter kolide.TeamFilter, hostIDs []uint, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
// The logic in this function should remain synchronized with
// host.Status and GenerateHostStatusStatistics
@ -26,8 +26,8 @@ func (d *Datastore) CountHostsInTargets(hostIDs []uint, labelIDs []uint, now tim
COALESCE(SUM(CASE WHEN DATE_ADD(seen_time, INTERVAL LEAST(distributed_interval, config_tls_refresh) + %d SECOND) > ? THEN 1 ELSE 0 END), 0) online,
COALESCE(SUM(CASE WHEN DATE_ADD(created_at, INTERVAL 1 DAY) >= ? THEN 1 ELSE 0 END), 0) new
FROM hosts h
WHERE (id IN (?) OR (id IN (SELECT DISTINCT host_id FROM label_membership WHERE label_id IN (?))))
`, kolide.OnlineIntervalBuffer, kolide.OnlineIntervalBuffer)
WHERE (id IN (?) OR (id IN (SELECT DISTINCT host_id FROM label_membership WHERE label_id IN (?)))) AND %s
`, kolide.OnlineIntervalBuffer, kolide.OnlineIntervalBuffer, d.whereFilterHostsByTeams(filter, "h"))
// Using -1 in the ID slices for the IN clause allows us to include the
// IN clause even if we have no IDs to use. -1 will not match the
@ -56,18 +56,20 @@ func (d *Datastore) CountHostsInTargets(hostIDs []uint, labelIDs []uint, now tim
return res, nil
}
func (d *Datastore) HostIDsInTargets(hostIDs []uint, labelIDs []uint) ([]uint, error) {
func (d *Datastore) HostIDsInTargets(filter kolide.TeamFilter, hostIDs, labelIDs []uint) ([]uint, error) {
if len(hostIDs) == 0 && len(labelIDs) == 0 {
// No need to query if no targets selected
return []uint{}, nil
}
sql := `
SELECT DISTINCT id
FROM hosts
WHERE (id IN (?) OR (id IN (SELECT host_id FROM label_membership WHERE label_id IN (?))))
ORDER BY id ASC
`
sql := fmt.Sprintf(`
SELECT DISTINCT id
FROM hosts
WHERE (id IN (?) OR (id IN (SELECT host_id FROM label_membership WHERE label_id IN (?)))) AND %s
ORDER BY id ASC
`,
d.whereFilterHostsByTeams(filter, "hosts"),
)
// Using -1 in the ID slices for the IN clause allows us to include the
// IN clause even if we have no IDs to use. -1 will not match the

View File

@ -57,7 +57,7 @@ type HostStore interface {
AuthenticateHost(nodeKey string) (*Host, error)
MarkHostSeen(host *Host, t time.Time) error
MarkHostsSeen(hostIDs []uint, t time.Time) error
SearchHosts(query string, omit ...uint) ([]*Host, error)
SearchHosts(filter TeamFilter, query string, omit ...uint) ([]*Host, error)
// CleanupIncomingHosts deletes hosts that have enrolled but never
// updated their status details. This clears dead "incoming hosts" that
// never complete their registration.

View File

@ -48,7 +48,7 @@ type LabelStore interface {
// it is in multiple of the provided labels.
ListUniqueHostsInLabels(labels []uint) ([]Host, error)
SearchLabels(query string, omit ...uint) ([]Label, error)
SearchLabels(filter TeamFilter, query string, omit ...uint) ([]Label, error)
// LabelIDsByName Retrieve the IDs associated with the given labels
LabelIDsByName(labels []string) ([]uint, error)

View File

@ -35,21 +35,21 @@ type TargetService interface {
// SearchTargets will accept a search query, a slice of IDs of hosts to omit,
// and a slice of IDs of labels to omit, and it will return a set of targets
// (hosts and label) which match the supplied search query.
SearchTargets(ctx context.Context, query string, selectedHostIDs []uint, selectedLabelIDs []uint) (*TargetSearchResults, error)
SearchTargets(ctx context.Context, query string, selectedHostIDs []uint, selectedLabelIDs []uint, includeObserver bool) (*TargetSearchResults, error)
// CountHostsInTargets returns the metrics of the hosts in the provided
// label and explicit host IDs.
CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint) (*TargetMetrics, error)
CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint, includeObserver bool) (*TargetMetrics, error)
}
type TargetStore interface {
// CountHostsInTargets returns the metrics of the hosts in the provided
// label and explicit host IDs.
CountHostsInTargets(hostIDs, labelIDs []uint, now time.Time) (TargetMetrics, error)
CountHostsInTargets(filter TeamFilter, hostIDs, labelIDs []uint, now time.Time) (TargetMetrics, error)
// HostIDsInTargets returns the host IDs of the hosts in the provided label
// and explicit host IDs. The returned host IDs should be sorted in
// ascending order.
HostIDsInTargets(hostIDs, labelIDs []uint) ([]uint, error)
HostIDsInTargets(filter TeamFilter, hostIDs, labelIDs []uint) ([]uint, error)
}
type TargetType int

View File

@ -6,6 +6,12 @@ import (
"time"
)
const (
RoleAdmin = "admin"
RoleMaintainer = "maintainer"
RoleObserver = "observer"
)
type TeamStore interface {
// NewTeam creates a new Team object in the store.
NewTeam(team *Team) (*Team, error)
@ -85,8 +91,8 @@ type TeamUser struct {
}
var teamRoles = map[string]bool{
"observer": true,
"maintainer": true,
RoleObserver: true,
RoleMaintainer: true,
}
// ValidTeamRole returns whether the role provided is valid for a team user.
@ -97,16 +103,16 @@ func ValidTeamRole(role string) bool {
// ValidTeamRoles returns the list of valid roles for a team user.
func ValidTeamRoles() []string {
var roles []string
for role, _ := range teamRoles {
for role := range teamRoles {
roles = append(roles, role)
}
return roles
}
var globalRoles = map[string]bool{
"observer": true,
"maintainer": true,
"admin": true,
RoleObserver: true,
RoleMaintainer: true,
RoleAdmin: true,
}
// ValidGlobalRole returns whether the role provided is valid for a global user.
@ -117,8 +123,17 @@ func ValidGlobalRole(role string) bool {
// ValidGlobalRoles returns the list of valid roles for a global user.
func ValidGlobalRoles() []string {
var roles []string
for role, _ := range globalRoles {
for role := range globalRoles {
roles = append(roles, role)
}
return roles
}
// TeamFilter is the filtering information passed to the datastore for queries
// that may be filtered by team.
type TeamFilter struct {
// User is the user to filter by.
User *User
// IncludeObserver determines whether to include teams the user is an observer on.
IncludeObserver bool
}

View File

@ -32,7 +32,7 @@ type MarkHostsSeenFunc func(hostIDs []uint, t time.Time) error
type CleanupIncomingHostsFunc func(t time.Time) error
type SearchHostsFunc func(query string, omit ...uint) ([]*kolide.Host, error)
type SearchHostsFunc func(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error)
type GenerateHostStatusStatisticsFunc func(now time.Time) (online uint, offline uint, mia uint, new uint, err error)
@ -147,9 +147,9 @@ func (s *HostStore) CleanupIncomingHosts(t time.Time) error {
return s.CleanupIncomingHostsFunc(t)
}
func (s *HostStore) SearchHosts(query string, omit ...uint) ([]*kolide.Host, error) {
func (s *HostStore) SearchHosts(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error) {
s.SearchHostsFuncInvoked = true
return s.SearchHostsFunc(query, omit...)
return s.SearchHostsFunc(filter, query, omit...)
}
func (s *HostStore) GenerateHostStatusStatistics(now time.Time) (online uint, offline uint, mia uint, new uint, err error) {

View File

@ -36,7 +36,7 @@ type ListHostsInLabelFunc func(lid uint, opt kolide.HostListOptions) ([]kolide.H
type ListUniqueHostsInLabelsFunc func(labels []uint) ([]kolide.Host, error)
type SearchLabelsFunc func(query string, omit ...uint) ([]kolide.Label, error)
type SearchLabelsFunc func(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error)
type LabelIDsByNameFunc func(labels []string) ([]uint, error)
@ -152,9 +152,9 @@ func (s *LabelStore) ListUniqueHostsInLabels(labels []uint) ([]kolide.Host, erro
return s.ListUniqueHostsInLabelsFunc(labels)
}
func (s *LabelStore) SearchLabels(query string, omit ...uint) ([]kolide.Label, error) {
func (s *LabelStore) SearchLabels(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error) {
s.SearchLabelsFuncInvoked = true
return s.SearchLabelsFunc(query, omit...)
return s.SearchLabelsFunc(filter, query, omit...)
}
func (s *LabelStore) LabelIDsByName(labels []string) ([]uint, error) {

View File

@ -10,8 +10,8 @@ import (
var _ kolide.TargetStore = (*TargetStore)(nil)
type CountHostsInTargetsFunc func(hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error)
type HostIDsInTargetsFunc func(hostIDs, labelIDs []uint) ([]uint, error)
type CountHostsInTargetsFunc func(filter kolide.TeamFilter, hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error)
type HostIDsInTargetsFunc func(filter kolide.TeamFilter, hostIDs, labelIDs []uint) ([]uint, error)
type TargetStore struct {
CountHostsInTargetsFunc CountHostsInTargetsFunc
@ -20,12 +20,12 @@ type TargetStore struct {
HostIDsInTargetsFuncInvoked bool
}
func (s *TargetStore) CountHostsInTargets(hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
func (s *TargetStore) CountHostsInTargets(filter kolide.TeamFilter, hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
s.CountHostsInTargetsFuncInvoked = true
return s.CountHostsInTargetsFunc(hostIDs, labelIDs, now)
return s.CountHostsInTargetsFunc(filter, hostIDs, labelIDs, now)
}
func (s *TargetStore) HostIDsInTargets(hostIDs, labelIDs []uint) ([]uint, error) {
func (s *TargetStore) HostIDsInTargets(filter kolide.TeamFilter, hostIDs, labelIDs []uint) ([]uint, error) {
s.HostIDsInTargetsFuncInvoked = true
return s.HostIDsInTargetsFunc(hostIDs, labelIDs)
return s.HostIDsInTargetsFunc(filter, hostIDs, labelIDs)
}

View File

@ -41,7 +41,7 @@ func packResponseForPack(ctx context.Context, svc kolide.Service, pack kolide.Pa
return nil, err
}
hostMetrics, err := svc.CountHostsInTargets(ctx, hosts, labelIDs)
hostMetrics, err := svc.CountHostsInTargets(ctx, hosts, labelIDs, false)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,9 @@ type searchTargetsRequest struct {
Labels []uint `json:"labels"`
Hosts []uint `json:"hosts"`
} `json:"selected"`
// IncludeObserver determines whether targets for which the user is only an
// observer should be included.
IncludeObserver bool `json:"include_observer"`
}
type hostSearchResult struct {
@ -51,7 +54,7 @@ func makeSearchTargetsEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(searchTargetsRequest)
results, err := svc.SearchTargets(ctx, req.Query, req.Selected.Hosts, req.Selected.Labels)
results, err := svc.SearchTargets(ctx, req.Query, req.Selected.Hosts, req.Selected.Labels, req.IncludeObserver)
if err != nil {
return searchTargetsResponse{Err: err}, nil
}
@ -83,7 +86,7 @@ func makeSearchTargetsEndpoint(svc kolide.Service) endpoint.Endpoint {
)
}
metrics, err := svc.CountHostsInTargets(ctx, req.Selected.Hosts, req.Selected.Labels)
metrics, err := svc.CountHostsInTargets(ctx, req.Selected.Hosts, req.Selected.Labels, req.IncludeObserver)
if err != nil {
return searchTargetsResponse{Err: err}, nil
}

View File

@ -88,7 +88,9 @@ func (svc service) NewDistributedQueryCampaign(ctx context.Context, queryString
}
}
hostIDs, err := svc.ds.HostIDsInTargets(hosts, labels)
filter := kolide.TeamFilter{User: vc.User}
hostIDs, err := svc.ds.HostIDsInTargets(filter, hosts, labels)
if err != nil {
return nil, errors.Wrap(err, "get target IDs")
}
@ -98,7 +100,7 @@ func (svc service) NewDistributedQueryCampaign(ctx context.Context, queryString
return nil, errors.Wrap(err, "run query")
}
campaign.Metrics, err = svc.ds.CountHostsInTargets(hosts, labels, time.Now())
campaign.Metrics, err = svc.ds.CountHostsInTargets(filter, hosts, labels, time.Now())
if err != nil {
return nil, errors.Wrap(err, "counting hosts")
}
@ -184,7 +186,8 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co
}
updateStatus := func() error {
metrics, err := svc.CountHostsInTargets(context.Background(), hostIDs, labelIDs)
// TODO use appropriate includeObserver value
metrics, err := svc.CountHostsInTargets(context.Background(), hostIDs, labelIDs, false)
if err != nil {
if err = conn.WriteJSONError("error retrieving target counts"); err != nil {
return errors.New("retrieve target counts")

View File

@ -1158,10 +1158,10 @@ func TestNewDistributedQueryCampaign(t *testing.T) {
return target, nil
}
ds.CountHostsInTargetsFunc = func(hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
ds.CountHostsInTargetsFunc = func(filter kolide.TeamFilter, hostIDs, labelIDs []uint, now time.Time) (kolide.TargetMetrics, error) {
return kolide.TargetMetrics{}, nil
}
ds.HostIDsInTargetsFunc = func(hostIDs, labelIDs []uint) ([]uint, error) {
ds.HostIDsInTargetsFunc = func(filter kolide.TeamFilter, hostIDs, labelIDs []uint) ([]uint, error) {
return []uint{1, 3, 5}, nil
}
lq.On("RunQuery", "21", "select year, month, day, hour, minutes, seconds from time", []uint{1, 3, 5}).Return(nil)

View File

@ -3,13 +3,21 @@ package service
import (
"context"
"github.com/fleetdm/fleet/server/contexts/viewer"
"github.com/fleetdm/fleet/server/kolide"
)
func (svc service) SearchTargets(ctx context.Context, query string, selectedHostIDs []uint, selectedLabelIDs []uint) (*kolide.TargetSearchResults, error) {
func (svc service) SearchTargets(ctx context.Context, query string, selectedHostIDs []uint, selectedLabelIDs []uint, includeObserver bool) (*kolide.TargetSearchResults, error) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, errNoContext
}
filter := kolide.TeamFilter{User: vc.User, IncludeObserver: includeObserver}
results := &kolide.TargetSearchResults{}
hosts, err := svc.ds.SearchHosts(query, selectedHostIDs...)
hosts, err := svc.ds.SearchHosts(filter, query, selectedHostIDs...)
if err != nil {
return nil, err
}
@ -18,7 +26,7 @@ func (svc service) SearchTargets(ctx context.Context, query string, selectedHost
results.Hosts = append(results.Hosts, *h)
}
labels, err := svc.ds.SearchLabels(query, selectedLabelIDs...)
labels, err := svc.ds.SearchLabels(filter, query, selectedLabelIDs...)
if err != nil {
return nil, err
}
@ -27,8 +35,15 @@ func (svc service) SearchTargets(ctx context.Context, query string, selectedHost
return results, nil
}
func (svc service) CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint) (*kolide.TargetMetrics, error) {
metrics, err := svc.ds.CountHostsInTargets(hostIDs, labelIDs, svc.clock.Now())
func (svc service) CountHostsInTargets(ctx context.Context, hostIDs []uint, labelIDs []uint, includeObserver bool) (*kolide.TargetMetrics, error) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, errNoContext
}
filter := kolide.TeamFilter{User: vc.User, IncludeObserver: includeObserver}
metrics, err := svc.ds.CountHostsInTargets(filter, hostIDs, labelIDs, svc.clock.Now())
if err != nil {
return nil, err
}

View File

@ -2,165 +2,68 @@ package service
import (
"context"
"fmt"
"testing"
"time"
"github.com/fleetdm/fleet/server/config"
"github.com/fleetdm/fleet/server/datastore/inmem"
"github.com/fleetdm/fleet/server/contexts/viewer"
"github.com/fleetdm/fleet/server/kolide"
"github.com/fleetdm/fleet/server/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)
func TestSearchTargets(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
ds := new(mock.Store)
svc, err := newTestService(ds, nil, nil)
require.Nil(t, err)
ctx := context.Background()
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: user})
h1, err := ds.NewHost(&kolide.Host{
HostName: "foo.local",
})
require.Nil(t, err)
hosts := []*kolide.Host{
{HostName: "foo.local"},
}
labels := []kolide.Label{
{
Name: "label foo",
Query: "query foo",
},
}
l1, err := ds.NewLabel(&kolide.Label{
Name: "label foo",
Query: "query foo",
})
require.Nil(t, err)
ds.SearchHostsFunc = func(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error) {
assert.Equal(t, user, filter.User)
return hosts, nil
}
ds.SearchLabelsFunc = func(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error) {
assert.Equal(t, user, filter.User)
return labels, nil
}
results, err := svc.SearchTargets(ctx, "foo", nil, nil)
require.Nil(t, err)
require.Len(t, results.Hosts, 1)
assert.Equal(t, h1.HostName, results.Hosts[0].HostName)
require.Len(t, results.Labels, 1)
assert.Equal(t, l1.Name, results.Labels[0].Name)
results, err := svc.SearchTargets(ctx, "foo", nil, nil, false)
require.NoError(t, err)
assert.Equal(t, *hosts[0], results.Hosts[0])
assert.Equal(t, labels[0], results.Labels[0])
}
func TestSearchWithOmit(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
ds := new(mock.Store)
svc, err := newTestService(ds, nil, nil)
require.Nil(t, err)
ctx := context.Background()
user := &kolide.User{GlobalRole: null.StringFrom(kolide.RoleAdmin)}
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: user})
h1, err := ds.NewHost(&kolide.Host{
HostName: "foo.local",
NodeKey: "1",
UUID: "1",
})
require.Nil(t, err)
h2, err := ds.NewHost(&kolide.Host{
HostName: "foobar.local",
NodeKey: "2",
UUID: "2",
})
require.Nil(t, err)
l1, err := ds.NewLabel(&kolide.Label{
Name: "label foo",
Query: "query foo",
})
require.Nil(t, err)
{
results, err := svc.SearchTargets(ctx, "foo", nil, nil)
require.Nil(t, err)
require.Len(t, results.Hosts, 2)
require.Len(t, results.Labels, 1)
assert.Equal(t, l1.Name, results.Labels[0].Name)
ds.SearchHostsFunc = func(filter kolide.TeamFilter, query string, omit ...uint) ([]*kolide.Host, error) {
assert.Equal(t, user, filter.User)
assert.Equal(t, []uint{1, 2}, omit)
return nil, nil
}
ds.SearchLabelsFunc = func(filter kolide.TeamFilter, query string, omit ...uint) ([]kolide.Label, error) {
assert.Equal(t, user, filter.User)
assert.Equal(t, []uint{3, 4}, omit)
return nil, nil
}
{
results, err := svc.SearchTargets(ctx, "foo", []uint{h2.ID}, nil)
require.Nil(t, err)
require.Len(t, results.Hosts, 1)
assert.Equal(t, h1.HostName, results.Hosts[0].HostName)
require.Len(t, results.Labels, 1)
assert.Equal(t, l1.Name, results.Labels[0].Name)
}
}
func TestSearchHostsInLabels(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
svc, err := newTestService(ds, nil, nil)
require.Nil(t, err)
ctx := context.Background()
h1, err := ds.NewHost(&kolide.Host{
HostName: "foo.local",
NodeKey: "1",
UUID: "1",
})
require.Nil(t, err)
h2, err := ds.NewHost(&kolide.Host{
HostName: "bar.local",
NodeKey: "2",
UUID: "2",
})
require.Nil(t, err)
h3, err := ds.NewHost(&kolide.Host{
HostName: "baz.local",
NodeKey: "3",
UUID: "3",
})
require.Nil(t, err)
l1, err := ds.NewLabel(&kolide.Label{
Name: "label foo",
Query: "query foo",
})
require.Nil(t, err)
require.NotZero(t, l1.ID)
for _, h := range []*kolide.Host{h1, h2, h3} {
err = ds.RecordLabelQueryExecutions(h, map[uint]bool{l1.ID: true}, time.Now())
assert.Nil(t, err)
}
results, err := svc.SearchTargets(ctx, "baz", nil, nil)
require.Nil(t, err)
require.Len(t, results.Hosts, 1)
assert.Equal(t, h3.HostName, results.Hosts[0].HostName)
}
func TestSearchResultsLimit(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
require.Nil(t, err)
svc, err := newTestService(ds, nil, nil)
require.Nil(t, err)
ctx := context.Background()
for i := 0; i < 15; i++ {
_, err := ds.NewHost(&kolide.Host{
HostName: fmt.Sprintf("foo.%d.local", i),
NodeKey: fmt.Sprintf("%d", i+1),
UUID: fmt.Sprintf("%d", i+1),
})
require.Nil(t, err)
}
targets, err := svc.SearchTargets(ctx, "foo", nil, nil)
require.Nil(t, err)
assert.Len(t, targets.Hosts, 10)
_, err = svc.SearchTargets(ctx, "foo", []uint{1, 2}, []uint{3, 4}, false)
require.Nil(t, err)
}

View File

@ -27,6 +27,24 @@ func ElementsMatchSkipID(t assert.TestingT, listA, listB interface{}, msgAndArgs
return ElementsMatchWithOptions(t, listA, listB, []cmp.Option{opt}, msgAndArgs)
}
// ElementsMatchSkipTimestampsID asserts that the elements match, skipping any field with
// name "ID", "CreatedAt", and "UpdatedAt". This is useful for comparing after DB insertion.
func ElementsMatchSkipTimestampsID(t assert.TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) {
opt := cmp.FilterPath(func(p cmp.Path) bool {
for _, ps := range p {
switch ps := ps.(type) {
case cmp.StructField:
switch ps.Name() {
case "ID", "UpdateCreateTimestamps", "CreateTimestamp", "UpdateTimestamp", "CreatedAt", "UpdatedAt":
return true
}
}
}
return false
}, cmp.Ignore())
return ElementsMatchWithOptions(t, listA, listB, []cmp.Option{opt}, msgAndArgs)
}
// The below functions adapted from
// https://github.com/stretchr/testify/blob/v1.7.0/assert/assertions.go#L895 by
// utilizing the options provided in github.com/google/go-cmp/cmp

0
tools/api/fleet/teams/create Normal file → Executable file
View File