mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
New APIs to add/remove manual labels to/from a host (#18283)
#16767 To create a manual label: ```sh cat labels.yml --- apiVersion: v1 kind: label spec: name: Manually Managed Example label_membership_type: manual hosts: - lucass-macbook-pro.local ``` To add/delete a manual label to/from a host: ``` curl -k -v -X POST -H "Authorization: Bearer $TEST_TOKEN" https://localhost:8080/api/latest/fleet/hosts/1/labels -d '{"labels": ["Manually Managed Example"]}' curl -k -v -X DELETE -H "Authorization: Bearer $TEST_TOKEN" https://localhost:8080/api/latest/fleet/hosts/1/labels -d '{"labels": ["Manually Managed Example"]}' ``` API draft changes: https://github.com/fleetdm/fleet/pull/16979/files Figma with error strings: https://www.figma.com/file/JiWoAiuHlkt76s3o3Uyz6h/%2316767-API-endpoint-for-updating-a-host's-manual-labels?type=design&node-id=2-130&mode=design&t=pxRPhrn6E1bOCrEd-0 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ~- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - ~[ ] If database migrations are included, checked table schema to confirm autoupdate~ - ~For database migrations:~ - ~[ ] Checked schema for all modified table for columns that will auto-update timestamps during migration.~ - ~[ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.~ - ~[ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).~ - [x] Manual QA for all new/changed functionality - ~For Orbit and Fleet Desktop changes:~ - ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux.~ - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
This commit is contained in:
parent
b45079e261
commit
e7f61305a9
14 changed files with 711 additions and 1 deletions
1
changes/16767-updating-host-labels
Normal file
1
changes/16767-updating-host-labels
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added endpoints to add/remove manual labels to/from a host. `POST /api/v1/fleet/hosts/:id/labels` and `DELETE /api/v1/fleet/hosts/:id/labels`.
|
||||
|
|
@ -40,6 +40,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
|
|||
| View a host by identifier | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| Target hosts using labels | ✅ | ✅ | ✅ | ✅ | |
|
||||
| Add/remove manual labels to/from hosts | | | ✅ | ✅ | ✅ |
|
||||
| Add and delete hosts | | | ✅ | ✅ | |
|
||||
| Transfer hosts between teams\* | | | ✅ | ✅ | ✅ |
|
||||
| Create, edit, and delete labels | | | ✅ | ✅ | ✅ |
|
||||
|
|
@ -124,6 +125,7 @@ Users with access to multiple teams can be assigned different roles for each tea
|
|||
| View a host by identifier | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| Target hosts using labels | ✅ | ✅ | ✅ | ✅ | |
|
||||
| Add/remove manual labels to/from hosts | | | ✅ | ✅ | ✅ |
|
||||
| Add and delete hosts | | | ✅ | ✅ | |
|
||||
| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| Filter hosts by software | ✅ | ✅ | ✅ | ✅ | |
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import input.subject
|
|||
read := "read"
|
||||
list := "list"
|
||||
write := "write"
|
||||
write_host_label := "write_host_label"
|
||||
|
||||
# User specific actions
|
||||
write_role := "write_role"
|
||||
|
|
@ -272,6 +273,13 @@ allow {
|
|||
action == write
|
||||
}
|
||||
|
||||
# Global admin, mantainers and gitops can write labels to hosts.
|
||||
allow {
|
||||
object.type == "host"
|
||||
subject.global_role == [admin, maintainer, gitops][_]
|
||||
action == write_host_label
|
||||
}
|
||||
|
||||
# Allow read for global observer and observer_plus, selective_read for gitops.
|
||||
allow {
|
||||
object.type == "host"
|
||||
|
|
@ -295,6 +303,13 @@ allow {
|
|||
action == write
|
||||
}
|
||||
|
||||
# Team admins, maintainers and gitops can write labels to hosts of their own team.
|
||||
allow {
|
||||
object.type == "host"
|
||||
team_role(subject, object.team_id) == [admin, maintainer, gitops][_]
|
||||
action == write_host_label
|
||||
}
|
||||
|
||||
# Allow read for host health for global admin/maintainer, team admins, observer.
|
||||
allow {
|
||||
object.type == "host_health"
|
||||
|
|
|
|||
|
|
@ -1015,3 +1015,36 @@ func (ds *Datastore) HostMemberOfAllLabels(ctx context.Context, hostID uint, lab
|
|||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) AddLabelsToHost(ctx context.Context, hostID uint, labelIDs []uint) error {
|
||||
if len(labelIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sql := `INSERT INTO label_membership (host_id, label_id) VALUES `
|
||||
sql += strings.Repeat(`(?, ?),`, len(labelIDs))
|
||||
sql = strings.TrimSuffix(sql, ",")
|
||||
sql += ` ON DUPLICATE KEY UPDATE updated_at = NOW()`
|
||||
args := make([]interface{}, 0, len(labelIDs)*2)
|
||||
for _, labelID := range labelIDs {
|
||||
args = append(args, hostID, labelID)
|
||||
}
|
||||
if _, err := ds.writer(ctx).ExecContext(ctx, sql, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "insert into label_membership")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) RemoveLabelsFromHost(ctx context.Context, hostID uint, labelIDs []uint) error {
|
||||
if len(labelIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sql := `DELETE FROM label_membership WHERE host_id = ? AND label_id IN (?)`
|
||||
sql, args, err := sqlx.In(sql, hostID, labelIDs)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "build label_membership IN query")
|
||||
}
|
||||
if _, err := ds.writer(ctx).ExecContext(ctx, sql, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete from label_membership")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ func TestLabels(t *testing.T) {
|
|||
{"ListHostsInLabelDiskEncryptionStatus", testListHostsInLabelDiskEncryptionStatus},
|
||||
{"HostMemberOfAllLabels", testHostMemberOfAllLabels},
|
||||
{"ListHostsInLabelOSSettings", testLabelsListHostsInLabelOSSettings},
|
||||
{"AddDeleteLabelsToFromHost", testAddDeleteLabelsToFromHost},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -1429,3 +1430,94 @@ func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) {
|
|||
checkHosts(t, hosts, []uint{h2.ID})
|
||||
})
|
||||
}
|
||||
|
||||
func testAddDeleteLabelsToFromHost(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
host1, err := ds.NewHost(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("1"),
|
||||
NodeKey: ptr.String("1"),
|
||||
UUID: "1",
|
||||
Hostname: "foo.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
host2, err := ds.NewHost(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("2"),
|
||||
NodeKey: ptr.String("2"),
|
||||
UUID: "2",
|
||||
Hostname: "bar.local",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.AddLabelsToHost(ctx, host1.ID, nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.RemoveLabelsFromHost(ctx, host1.ID, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
label1, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "label1",
|
||||
Query: "SELECT 1;",
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
label2, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "label2",
|
||||
Query: "SELECT 2;",
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Removing a label and multiple labels that the host is not a member of.
|
||||
err = ds.RemoveLabelsFromHost(ctx, host1.ID, []uint{label1.ID})
|
||||
require.NoError(t, err)
|
||||
err = ds.RemoveLabelsFromHost(ctx, host1.ID, []uint{label1.ID, label2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Adding and removing labels.
|
||||
err = ds.AddLabelsToHost(ctx, host1.ID, []uint{label1.ID})
|
||||
require.NoError(t, err)
|
||||
getLabelUpdatedAt := func(updatedAt *time.Time) func(q sqlx.ExtContext) error {
|
||||
return func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, updatedAt, `SELECT updated_at FROM label_membership WHERE host_id = ? AND label_id = ?`, host1.ID, label1.ID)
|
||||
}
|
||||
}
|
||||
var labelUpdatedAt1 time.Time
|
||||
ExecAdhocSQL(t, ds, getLabelUpdatedAt(&labelUpdatedAt1))
|
||||
time.Sleep(1 * time.Second)
|
||||
// Add a label that the host is already member of.
|
||||
err = ds.AddLabelsToHost(ctx, host1.ID, []uint{label1.ID})
|
||||
require.NoError(t, err)
|
||||
var labelUpdatedAt2 time.Time
|
||||
ExecAdhocSQL(t, ds, getLabelUpdatedAt(&labelUpdatedAt2))
|
||||
require.True(t, labelUpdatedAt2.After(labelUpdatedAt1))
|
||||
labels, err := ds.ListLabelsForHost(ctx, host1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 1)
|
||||
require.Equal(t, "label1", labels[0].Name)
|
||||
labels2, err := ds.ListLabelsForHost(ctx, host2.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, labels2)
|
||||
|
||||
// Removing a label that the host is a member of
|
||||
// and one that the host is not a member of.
|
||||
err = ds.RemoveLabelsFromHost(ctx, host1.ID, []uint{label1.ID, label2.ID})
|
||||
require.NoError(t, err)
|
||||
labels, err = ds.ListLabelsForHost(ctx, host1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, labels)
|
||||
|
||||
// Add and remove multiple labels.
|
||||
err = ds.AddLabelsToHost(ctx, host1.ID, []uint{label1.ID, label2.ID})
|
||||
require.NoError(t, err)
|
||||
labels, err = ds.ListLabelsForHost(ctx, host1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 2)
|
||||
err = ds.RemoveLabelsFromHost(ctx, host1.ID, []uint{label1.ID, label2.ID})
|
||||
require.NoError(t, err)
|
||||
labels, err = ds.ListLabelsForHost(ctx, host1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, labels)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const (
|
|||
ActionList = "list"
|
||||
// ActionWrite refers to writing (CRUD operations) an entity.
|
||||
ActionWrite = "write"
|
||||
// ActionWriteHostLabel refers to writing labels on hosts.
|
||||
ActionWriteHostLabel = "write_host_label"
|
||||
|
||||
//
|
||||
// User specific actions
|
||||
|
|
|
|||
|
|
@ -176,6 +176,13 @@ type Datastore interface {
|
|||
// GetLabelSpec returns the spec for the named label.
|
||||
GetLabelSpec(ctx context.Context, name string) (*LabelSpec, error)
|
||||
|
||||
// AddLabelsToHost adds the given label IDs membership to the host.
|
||||
// If a host is already a member of the label then this will update the row's updated_at.
|
||||
AddLabelsToHost(ctx context.Context, hostID uint, labelIDs []uint) error
|
||||
// RemoveLabelsFromHost removes the given label IDs membership from the host.
|
||||
// If a host is already not a member of a label then such label will be ignored.
|
||||
RemoveLabelsFromHost(ctx context.Context, hostID uint, labelIDs []uint) error
|
||||
|
||||
NewLabel(ctx context.Context, Label *Label, opts ...OptionalArg) (*Label, error)
|
||||
SaveLabel(ctx context.Context, label *Label) (*Label, error)
|
||||
DeleteLabel(ctx context.Context, name string) error
|
||||
|
|
|
|||
|
|
@ -388,6 +388,21 @@ type Service interface {
|
|||
|
||||
HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error)
|
||||
|
||||
// AddLabelsToHost adds the given label names to the host's label membership.
|
||||
//
|
||||
// If a host is already a member of one of the labels then this operation will only
|
||||
// update the membership row update time.
|
||||
//
|
||||
// Returns an error if any of the labels does not exist or if any of the labels
|
||||
// are not manual.
|
||||
AddLabelsToHost(ctx context.Context, id uint, labels []string) error
|
||||
// RemoveLabelsFromHost removes the given label names from the host's label membership.
|
||||
// Labels that the host are already not a member of are ignored.
|
||||
//
|
||||
// Returns an error if any of the labels does not exist or if any of the labels
|
||||
// are not manual.
|
||||
RemoveLabelsFromHost(ctx context.Context, id uint, labels []string) 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 name and version.
|
||||
// Name cannot be used without version, and conversely, version cannot be used without name.
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ type GetLabelSpecsFunc func(ctx context.Context) ([]*fleet.LabelSpec, error)
|
|||
|
||||
type GetLabelSpecFunc func(ctx context.Context, name string) (*fleet.LabelSpec, error)
|
||||
|
||||
type AddLabelsToHostFunc func(ctx context.Context, hostID uint, labelIDs []uint) error
|
||||
|
||||
type RemoveLabelsFromHostFunc func(ctx context.Context, hostID uint, labelIDs []uint) error
|
||||
|
||||
type NewLabelFunc func(ctx context.Context, Label *fleet.Label, opts ...fleet.OptionalArg) (*fleet.Label, error)
|
||||
|
||||
type SaveLabelFunc func(ctx context.Context, label *fleet.Label) (*fleet.Label, error)
|
||||
|
|
@ -1071,6 +1075,12 @@ type DataStore struct {
|
|||
GetLabelSpecFunc GetLabelSpecFunc
|
||||
GetLabelSpecFuncInvoked bool
|
||||
|
||||
AddLabelsToHostFunc AddLabelsToHostFunc
|
||||
AddLabelsToHostFuncInvoked bool
|
||||
|
||||
RemoveLabelsFromHostFunc RemoveLabelsFromHostFunc
|
||||
RemoveLabelsFromHostFuncInvoked bool
|
||||
|
||||
NewLabelFunc NewLabelFunc
|
||||
NewLabelFuncInvoked bool
|
||||
|
||||
|
|
@ -2623,6 +2633,20 @@ func (s *DataStore) GetLabelSpec(ctx context.Context, name string) (*fleet.Label
|
|||
return s.GetLabelSpecFunc(ctx, name)
|
||||
}
|
||||
|
||||
func (s *DataStore) AddLabelsToHost(ctx context.Context, hostID uint, labelIDs []uint) error {
|
||||
s.mu.Lock()
|
||||
s.AddLabelsToHostFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.AddLabelsToHostFunc(ctx, hostID, labelIDs)
|
||||
}
|
||||
|
||||
func (s *DataStore) RemoveLabelsFromHost(ctx context.Context, hostID uint, labelIDs []uint) error {
|
||||
s.mu.Lock()
|
||||
s.RemoveLabelsFromHostFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.RemoveLabelsFromHostFunc(ctx, hostID, labelIDs)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewLabel(ctx context.Context, Label *fleet.Label, opts ...fleet.OptionalArg) (*fleet.Label, error) {
|
||||
s.mu.Lock()
|
||||
s.NewLabelFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -396,6 +396,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.GET("/api/_version_/fleet/os_versions/{id:[0-9]+}", getOSVersionEndpoint, getOSVersionRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/queries/{query_id:[0-9]+}", getHostQueryReportEndpoint, getHostQueryReportRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/health", getHostHealthEndpoint, getHostHealthRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", addLabelsToHostEndpoint, addLabelsToHostRequest{})
|
||||
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", removeLabelsFromHostEndpoint, removeLabelsFromHostRequest{})
|
||||
|
||||
ue.GET("/api/_version_/fleet/hosts/summary/mdm", getHostMDMSummary, getHostMDMSummaryRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/mdm", getHostMDM, getHostMDMRequest{})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
|
|
@ -2261,3 +2262,174 @@ func hostListOptionsFromFilters(filter *map[string]interface{}) (*fleet.HostList
|
|||
|
||||
return &opt, labelID, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Host Labels
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type addLabelsToHostRequest struct {
|
||||
ID uint `url:"id"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
type addLabelsToHostResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r addLabelsToHostResponse) error() error { return r.Err }
|
||||
|
||||
func addLabelsToHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*addLabelsToHostRequest)
|
||||
if err := svc.AddLabelsToHost(ctx, req.ID, req.Labels); err != nil {
|
||||
return addLabelsToHostResponse{Err: err}, nil
|
||||
}
|
||||
return addLabelsToHostResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) AddLabelsToHost(ctx context.Context, id uint, labelNames []string) error {
|
||||
host, err := svc.ds.HostLite(ctx, id)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return ctxerr.Wrap(ctx, err, "load host")
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, host, fleet.ActionWriteHostLabel); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
labelIDs, err := svc.validateLabelNames(ctx, "add", labelNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(labelIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := svc.ds.AddLabelsToHost(ctx, host.ID, labelIDs); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "add labels to host")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type removeLabelsFromHostRequest struct {
|
||||
ID uint `url:"id"`
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
type removeLabelsFromHostResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r removeLabelsFromHostResponse) error() error { return r.Err }
|
||||
|
||||
func removeLabelsFromHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*removeLabelsFromHostRequest)
|
||||
if err := svc.RemoveLabelsFromHost(ctx, req.ID, req.Labels); err != nil {
|
||||
return removeLabelsFromHostResponse{Err: err}, nil
|
||||
}
|
||||
return removeLabelsFromHostResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) RemoveLabelsFromHost(ctx context.Context, id uint, labelNames []string) error {
|
||||
host, err := svc.ds.HostLite(ctx, id)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return ctxerr.Wrap(ctx, err, "load host")
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, host, fleet.ActionWriteHostLabel); err != nil {
|
||||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
labelIDs, err := svc.validateLabelNames(ctx, "remove", labelNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(labelIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := svc.ds.RemoveLabelsFromHost(ctx, host.ID, labelIDs); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "remove labels from host")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) validateLabelNames(ctx context.Context, action string, labelNames []string) ([]uint, error) {
|
||||
if len(labelNames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
labelNames = server.RemoveDuplicatesFromSlice(labelNames)
|
||||
|
||||
// Filter out empty label string.
|
||||
for i, labelName := range labelNames {
|
||||
if labelName == "" {
|
||||
labelNames = append(labelNames[:i], labelNames[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(labelNames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
labels, err := svc.ds.LabelIDsByName(ctx, labelNames)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting label IDs by name")
|
||||
}
|
||||
|
||||
var labelsNotFound []string
|
||||
for _, labelName := range labelNames {
|
||||
if _, ok := labels[labelName]; !ok {
|
||||
labelsNotFound = append(labelsNotFound, "\""+labelName+"\"")
|
||||
}
|
||||
}
|
||||
|
||||
if len(labelsNotFound) > 0 {
|
||||
sort.Slice(labelsNotFound, func(i, j int) bool {
|
||||
// Ignore quotes to sort.
|
||||
return labelsNotFound[i][1:len(labelsNotFound[i])-1] < labelsNotFound[j][1:len(labelsNotFound[j])-1]
|
||||
})
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf(
|
||||
"Couldn't %s labels. Labels not found: %s. All labels must exist.",
|
||||
action,
|
||||
strings.Join(labelsNotFound, ", "),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
var dynamicLabels []string
|
||||
for labelName, labelID := range labels {
|
||||
label, err := svc.ds.Label(ctx, labelID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "load label from id")
|
||||
}
|
||||
if label.LabelMembershipType != fleet.LabelMembershipTypeManual {
|
||||
dynamicLabels = append(dynamicLabels, "\""+labelName+"\"")
|
||||
}
|
||||
}
|
||||
|
||||
if len(dynamicLabels) > 0 {
|
||||
sort.Slice(dynamicLabels, func(i, j int) bool {
|
||||
// Ignore quotes to sort.
|
||||
return dynamicLabels[i][1:len(dynamicLabels[i])-1] < dynamicLabels[j][1:len(dynamicLabels[j])-1]
|
||||
})
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf(
|
||||
"Couldn't %s labels. Labels are dynamic: %s. Dynamic labels can not be assigned to hosts manually.",
|
||||
action,
|
||||
strings.Join(dynamicLabels, ", "),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
labelIDs := make([]uint, 0, len(labels))
|
||||
for _, labelID := range labels {
|
||||
labelIDs = append(labelIDs, labelID)
|
||||
}
|
||||
|
||||
return labelIDs, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11010,3 +11010,303 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() {
|
|||
require.Empty(t, listResp.Activities)
|
||||
require.Equal(t, &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false}, listResp.Meta)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestAddingRemovingManualLabels() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
team1, err := s.ds.NewTeam(ctx, &fleet.Team{
|
||||
Name: "team1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
newGlobalUserFunc := func(email string, globalRole string) *fleet.User {
|
||||
user := &fleet.User{
|
||||
Name: email,
|
||||
Email: email,
|
||||
GlobalRole: &globalRole,
|
||||
}
|
||||
err = user.SetPassword(test.GoodPassword, 10, 10)
|
||||
require.NoError(t, err)
|
||||
user, err = s.ds.NewUser(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
return user
|
||||
}
|
||||
newTeamUserFunc := func(email string, team *fleet.Team, teamRole string) *fleet.User {
|
||||
user := &fleet.User{
|
||||
Name: email,
|
||||
Email: email,
|
||||
Teams: []fleet.UserTeam{
|
||||
{
|
||||
Team: *team,
|
||||
Role: teamRole,
|
||||
},
|
||||
},
|
||||
}
|
||||
err = user.SetPassword(test.GoodPassword, 10, 10)
|
||||
require.NoError(t, err)
|
||||
user, err = s.ds.NewUser(context.Background(), user)
|
||||
require.NoError(t, err)
|
||||
return user
|
||||
}
|
||||
globalObserver := newGlobalUserFunc("global.observer@example.com", fleet.RoleObserver)
|
||||
teamAdmin := newTeamUserFunc("team.admin@example.com", team1, fleet.RoleAdmin)
|
||||
teamObserver := newTeamUserFunc("team.observer@example.com", team1, fleet.RoleObserver)
|
||||
|
||||
newHostFunc := func(name string, teamID *uint) *fleet.Host {
|
||||
host, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
NodeKey: ptr.String(name),
|
||||
UUID: name,
|
||||
Hostname: "foo.local." + name,
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host)
|
||||
return host
|
||||
}
|
||||
host1 := newHostFunc("host1", nil)
|
||||
host2 := newHostFunc("host2", nil)
|
||||
teamHost2 := newHostFunc("teamHost2", &team1.ID)
|
||||
|
||||
ls, err := s.ds.LabelIDsByName(ctx, []string{"All Hosts"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ls, 1)
|
||||
allHostsLabelID, ok := ls["All Hosts"]
|
||||
require.True(t, ok)
|
||||
require.NotZero(t, allHostsLabelID)
|
||||
|
||||
dynamicLabel1, err := s.ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "dynamicLabel1",
|
||||
Query: "SELECT 1;",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
manualLabel1, err := s.ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "manualLabel1",
|
||||
Query: "SELECT 2;",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
manualLabel2, err := s.ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "manualLabel2",
|
||||
Query: "SELECT 3;",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.RecordLabelQueryExecutions(context.Background(), host1, map[uint]*bool{allHostsLabelID: ptr.Bool(true)}, time.Now(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
getHostLabels := func(host *fleet.Host) []string {
|
||||
var hostResp getHostResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
|
||||
var labels []string
|
||||
for _, label := range hostResp.Host.Labels {
|
||||
labels = append(labels, label.Name)
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
hostLabels1 := getHostLabels(host1)
|
||||
require.Len(t, hostLabels1, 1)
|
||||
require.Equal(t, "All Hosts", hostLabels1[0])
|
||||
|
||||
// No labels or empty labels is a no-op.
|
||||
var addLabelsToHostResp addLabelsToHostResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID),
|
||||
json.RawMessage(`{}`), http.StatusOK, &addLabelsToHostResp,
|
||||
)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
var removeLabelsFromHostResp removeLabelsFromHostResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{""},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{"", ""},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
|
||||
// A dynamic buitin label should fail to be added.
|
||||
res := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{"All Hosts"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't add labels. Labels are dynamic: \"All Hosts\". Dynamic labels can not be assigned to hosts manually.")
|
||||
// An inexistent label should fail to be added.
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{"manualLabel2", "does not exist"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't add labels. Labels not found: \"does not exist\". All labels must exist.")
|
||||
// Multiple inexistent labels should fail to be added.
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{"manualLabel2", "does not exist", "does not exist 2"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't add labels. Labels not found: \"does not exist\", \"does not exist 2\". All labels must exist.")
|
||||
// A dynamic non-builtin label should fail to be added.
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{dynamicLabel1.Name},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't add labels. Labels are dynamic: \"dynamicLabel1\". Dynamic labels can not be assigned to hosts manually.")
|
||||
// Multiple dynamic labels should fail to be added.
|
||||
res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{"All Hosts", dynamicLabel1.Name},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't add labels. Labels are dynamic: \"All Hosts\", \"dynamicLabel1\". Dynamic labels can not be assigned to hosts manually.")
|
||||
|
||||
// A dynamic builtin label should fail to be deleted.
|
||||
res = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{"All Hosts"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't remove labels. Labels are dynamic: \"All Hosts\". Dynamic labels can not be assigned to hosts manually.")
|
||||
// An inexistent label should fail to be deleted.
|
||||
res = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel2.Name, "does not exist"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't remove labels. Labels not found: \"does not exist\". All labels must exist.")
|
||||
// Multiple inexistent labels should fail to be deleted.
|
||||
res = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel2.Name, "does not exist", "does not exist 2"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't remove labels. Labels not found: \"does not exist\", \"does not exist 2\". All labels must exist.")
|
||||
// Multiple dynamic labels should fail to be deleted.
|
||||
res = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel2.Name, dynamicLabel1.Name, "All Hosts"},
|
||||
}, http.StatusBadRequest)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Couldn't remove labels. Labels are dynamic: \"All Hosts\", \"dynamicLabel1\". Dynamic labels can not be assigned to hosts manually.")
|
||||
|
||||
// Add two manual labels to a host.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
// Add the same manual labels to a host should succeed.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
|
||||
hostLabels1 = getHostLabels(host1)
|
||||
require.Len(t, hostLabels1, 3)
|
||||
require.Equal(t, "All Hosts", hostLabels1[0])
|
||||
require.Equal(t, manualLabel1.Name, hostLabels1[1])
|
||||
require.Equal(t, manualLabel2.Name, hostLabels1[2])
|
||||
hostLabels2 := getHostLabels(host2)
|
||||
require.Empty(t, hostLabels2)
|
||||
|
||||
// Remove the two manual labels from the host.
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
// Remove the same manual labels from the host again.
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
|
||||
hostLabels1 = getHostLabels(host1)
|
||||
require.Len(t, hostLabels1, 1)
|
||||
require.Equal(t, "All Hosts", hostLabels1[0])
|
||||
|
||||
// Add same label, should deduplicate.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel1.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
|
||||
hostLabels1 = getHostLabels(host1)
|
||||
require.Len(t, hostLabels1, 2)
|
||||
require.Equal(t, "All Hosts", hostLabels1[0])
|
||||
require.Equal(t, manualLabel1.Name, hostLabels1[1])
|
||||
|
||||
// Adding an already added label should work.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
|
||||
hostLabels1 = getHostLabels(host1)
|
||||
require.Len(t, hostLabels1, 3)
|
||||
require.Equal(t, "All Hosts", hostLabels1[0])
|
||||
require.Equal(t, manualLabel1.Name, hostLabels1[1])
|
||||
require.Equal(t, manualLabel2.Name, hostLabels1[2])
|
||||
|
||||
// Delete same label, should deduplicate.
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel1.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
|
||||
// Deleting a non-member label (manualLabel1) should work.
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
|
||||
hostLabels1 = getHostLabels(host1)
|
||||
require.Len(t, hostLabels1, 1)
|
||||
require.Equal(t, "All Hosts", hostLabels1[0])
|
||||
|
||||
// Add to non-existent host
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", 999), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusNotFound, &addLabelsToHostResp)
|
||||
// Delete from non-existent host
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", 999), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name, manualLabel2.Name},
|
||||
}, http.StatusNotFound, &removeLabelsFromHostResp)
|
||||
|
||||
// Add labels to team host.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
|
||||
// A global observer should not be allowed to add/remove a label.
|
||||
oldToken := s.token
|
||||
s.token = s.getTestToken(globalObserver.Email, test.GoodPassword)
|
||||
t.Cleanup(func() {
|
||||
s.token = oldToken
|
||||
})
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &addLabelsToHostResp)
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &removeLabelsFromHostResp)
|
||||
|
||||
// A team observer should not be allowed to add/remove a label.
|
||||
s.token = s.getTestToken(teamObserver.Email, test.GoodPassword)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &addLabelsToHostResp)
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &removeLabelsFromHostResp)
|
||||
|
||||
// A team admin should not be allowed to add/remove a label for a global host.
|
||||
s.token = s.getTestToken(teamAdmin.Email, test.GoodPassword)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &addLabelsToHostResp)
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", host1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &removeLabelsFromHostResp)
|
||||
|
||||
// A team admin should be allowed to add/remove a label for a team host.
|
||||
s.token = s.getTestToken(teamAdmin.Email, test.GoodPassword)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
teamHost2Labels := getHostLabels(teamHost2)
|
||||
require.Len(t, teamHost2Labels, 1)
|
||||
require.Equal(t, manualLabel1.Name, teamHost2Labels[0])
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHost2.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
teamHost2Labels = getHostLabels(teamHost2)
|
||||
require.Empty(t, teamHost2Labels)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4222,6 +4222,19 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() {
|
|||
Name: "Zoo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team1Host, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
NodeKey: ptr.String(t.Name() + "2"),
|
||||
UUID: t.Name() + "2",
|
||||
Hostname: strings.Replace(t.Name()+"zoo.local", "/", "_", -1),
|
||||
TeamID: &t1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
globalHost, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
NodeKey: ptr.String(t.Name() + "3"),
|
||||
UUID: t.Name() + "3",
|
||||
Hostname: strings.Replace(t.Name()+"global.local", "/", "_", -1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
acr := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"webhook_settings": {
|
||||
|
|
@ -4328,6 +4341,12 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() {
|
|||
require.NoError(t, u3.SetPassword(test.GoodPassword, 10, 10))
|
||||
_, err = s.ds.NewUser(context.Background(), u3)
|
||||
require.NoError(t, err)
|
||||
manualLabel1, err := s.ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "manualLabel1",
|
||||
Query: "SELECT 2;",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
//
|
||||
// Start running permission tests with user gitops1.
|
||||
|
|
@ -4407,6 +4426,16 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() {
|
|||
require.True(t, acr.AppConfig.WebhookSettings.VulnerabilitiesWebhook.Enable)
|
||||
require.Equal(t, "https://foobar.example.com", acr.AppConfig.WebhookSettings.VulnerabilitiesWebhook.DestinationURL)
|
||||
|
||||
// Attempt to add/remove manual labels to/from a host.
|
||||
var addLabelsToHostResp addLabelsToHostResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", h1.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
var removeLabelsFromHostResp removeLabelsFromHostResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", h1.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
|
||||
// Attempt to run live queries synchronously, should fail.
|
||||
s.DoJSON("GET", "/api/latest/fleet/queries/run", runLiveQueryRequest{
|
||||
HostIDs: []uint{h1.ID},
|
||||
|
|
@ -4779,6 +4808,22 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() {
|
|||
// Attempt to remove a query from the team's schedule, should allow.
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule/%d", t1.ID, ttsqr.Scheduled.ID), deleteTeamScheduleRequest{}, http.StatusOK, &deleteTeamScheduleResponse{})
|
||||
|
||||
// Attempt to add/remove a manual label from a team host, should allow.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", team1Host.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", team1Host.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
|
||||
// Attempt to add/remove a manual label from a global host, should not allow.
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", globalHost.ID), addLabelsToHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &addLabelsToHostResp)
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", globalHost.ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{manualLabel1.Name},
|
||||
}, http.StatusForbidden, &removeLabelsFromHostResp)
|
||||
|
||||
// Attempt to read the global schedule, should fail.
|
||||
s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusForbidden, &getGlobalScheduleResponse{})
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ func AddBuiltinLabels(t *testing.T, ds fleet.Datastore) {
|
|||
Name: "All Hosts",
|
||||
Query: "select 1",
|
||||
LabelType: fleet.LabelTypeBuiltIn,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
},
|
||||
{
|
||||
Name: "macOS",
|
||||
|
|
|
|||
Loading…
Reference in a new issue