mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Add additional test coverage for team labels (#37793)
There will be one more follow-up for set-aside behavior.
This commit is contained in:
parent
6080ca6a1e
commit
ec4c1088d9
8 changed files with 1159 additions and 78 deletions
|
|
@ -362,7 +362,7 @@ allow {
|
|||
# Team admins, maintainers, observer_plus, observers and gitops can read global labels.
|
||||
allow {
|
||||
object.type == "label"
|
||||
is_null(object.team_id)
|
||||
any([is_null(object.team_id), object.team_id == 0]) # allow specifying team ID 0 for listing exclusively global labels
|
||||
# If role is admin, maintainer, observer_plus or observer on any team.
|
||||
team_role(subject, subject.teams[_].id) == [admin, maintainer, observer_plus, observer, gitops][_]
|
||||
action == read
|
||||
|
|
|
|||
|
|
@ -346,8 +346,7 @@ func (ds *Datastore) UpdateLabelMembershipByHostIDs(ctx context.Context, label f
|
|||
// Split hostIds into batches to avoid parameter limit in MySQL.
|
||||
for _, hostIds := range batchHostIds(hostIds) {
|
||||
if label.TeamID != nil { // team labels can only be applied to hosts on that team
|
||||
hostTeamCheckSql := `SELECT COUNT(id) FROM hosts WHERE team_id != ? AND id IN (` +
|
||||
strings.TrimRight(strings.Repeat("?,", len(hostIds)), ",") + ")"
|
||||
hostTeamCheckSql := `SELECT COUNT(id) FROM hosts WHERE (team_id != ? OR team_id IS NULL) AND id IN (?)`
|
||||
hostTeamCheckSql, args, err := sqlx.In(hostTeamCheckSql, label.TeamID, hostIds)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "build host team membership check IN statement")
|
||||
|
|
@ -502,7 +501,7 @@ func (ds *Datastore) GetLabelSpecs(ctx context.Context, filter fleet.TeamFilter)
|
|||
func (ds *Datastore) GetLabelSpec(ctx context.Context, filter fleet.TeamFilter, name string) (*fleet.LabelSpec, error) {
|
||||
var specs []*fleet.LabelSpec
|
||||
query, params, err := applyLabelTeamFilter(`
|
||||
SELECT l.id, l.name, l.description, l.query, l.platform, l.label_type, l.label_membership_type
|
||||
SELECT l.id, l.name, l.description, l.query, l.platform, l.label_type, l.label_membership_type, l.team_id
|
||||
FROM labels l
|
||||
WHERE l.name = ?`, filter, name)
|
||||
if err != nil {
|
||||
|
|
@ -608,7 +607,7 @@ func (ds *Datastore) DeleteLabel(ctx context.Context, name string, filter fleet.
|
|||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var labelID uint
|
||||
|
||||
query, params, err := applyLabelTeamFilter(`select id FROM labels WHERE name = ?`, filter, name)
|
||||
query, params, err := applyLabelTeamFilter(`select l.id FROM labels l WHERE l.name = ?`, filter, name)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting label id to delete")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,8 +233,6 @@ func testLabelsAddAllHosts(deferred bool, t *testing.T, db *Datastore) {
|
|||
}
|
||||
|
||||
func testLabelsSearch(t *testing.T, db *Datastore) {
|
||||
// TODO test team filtering
|
||||
|
||||
specs := []*fleet.LabelSpec{
|
||||
{ID: 1, Name: "foo"},
|
||||
{ID: 2, Name: "bar"},
|
||||
|
|
@ -256,8 +254,6 @@ func testLabelsSearch(t *testing.T, db *Datastore) {
|
|||
err := db.ApplyLabelSpecs(context.Background(), specs)
|
||||
require.Nil(t, err)
|
||||
|
||||
// TODO add team checking
|
||||
|
||||
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
filter := fleet.TeamFilter{User: user}
|
||||
|
||||
|
|
@ -287,11 +283,81 @@ func testLabelsSearch(t *testing.T, db *Datastore) {
|
|||
require.Nil(t, err)
|
||||
assert.Len(t, labels, 1)
|
||||
assert.Contains(t, labels, &all.Label)
|
||||
|
||||
// Test team filtering
|
||||
team1, err := db.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := db.NewTeam(context.Background(), &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create team labels
|
||||
team1Label, err := db.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team1-foo",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
team2Label, err := db.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team2-foo",
|
||||
Query: "SELECT 2",
|
||||
TeamID: &team2.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global admin should see all labels (global + team labels), including the All Hosts label
|
||||
labels, err = db.SearchLabels(context.Background(), filter, "foo")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, labels, 5) // foo, foo-bar, All Hosts, team1-foo, team2-foo
|
||||
|
||||
// Filter to team1 only - should see global labels + team1 labels
|
||||
team1Filter := fleet.TeamFilter{User: user, TeamID: &team1.ID}
|
||||
labels, err = db.SearchLabels(context.Background(), team1Filter, "foo")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, labels, 4) // foo, foo-bar, All Hosts, team1-foo
|
||||
foundTeam1Label := false
|
||||
foundTeam2Label := false
|
||||
for _, l := range labels {
|
||||
if l.ID == team1Label.ID {
|
||||
foundTeam1Label = true
|
||||
}
|
||||
if l.ID == team2Label.ID {
|
||||
foundTeam2Label = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundTeam1Label, "team1 label should be found")
|
||||
assert.False(t, foundTeam2Label, "team2 label should not be found")
|
||||
|
||||
// Filter to global only (team_id = 0)
|
||||
globalOnlyFilter := fleet.TeamFilter{User: user, TeamID: ptr.Uint(0)}
|
||||
labels, err = db.SearchLabels(context.Background(), globalOnlyFilter, "foo")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, labels, 3) // foo, foo-bar, All Hosts
|
||||
for _, l := range labels {
|
||||
assert.Nil(t, l.TeamID, "should only have global labels")
|
||||
}
|
||||
|
||||
// Team user can only see their team's labels + global labels
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleObserver}}}
|
||||
team1UserFilter := fleet.TeamFilter{User: team1User}
|
||||
labels, err = db.SearchLabels(context.Background(), team1UserFilter, "foo")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, labels, 4) // foo, foo-bar, All Hosts, team1-foo
|
||||
for _, l := range labels {
|
||||
if l.TeamID != nil {
|
||||
assert.Equal(t, team1.ID, *l.TeamID, "team user should only see their team's labels")
|
||||
}
|
||||
}
|
||||
|
||||
// Team user trying to access another team's labels should fail
|
||||
team1UserTeam2Filter := fleet.TeamFilter{User: team1User, TeamID: &team2.ID}
|
||||
_, err = db.SearchLabels(context.Background(), team1UserTeam2Filter, "foo")
|
||||
require.ErrorContains(t, err, errInaccessibleTeam.Error()) // not ErrorIs due to UserError wrapping
|
||||
}
|
||||
|
||||
func testLabelsListHostsInLabel(t *testing.T, db *Datastore) {
|
||||
// TODO test label filtering
|
||||
|
||||
h1, err := db.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
|
|
@ -385,6 +451,56 @@ func testLabelsListHostsInLabel(t *testing.T, db *Datastore) {
|
|||
listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMIDFilter: ptr.Uint(kandjiID)}, 1)
|
||||
listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMSimpleMDM)}, 2)
|
||||
listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{MDMNameFilter: ptr.String(fleet.WellKnownMDMSimpleMDM), MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusEnrolled}, 1)
|
||||
|
||||
// Test team label filtering
|
||||
team1, err := db.NewTeam(context.Background(), &fleet.Team{Name: "team1_listhosts"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a team label
|
||||
teamLabel, err := db.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team1-label",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a host on team1
|
||||
h4, err := db.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("4"),
|
||||
NodeKey: ptr.String("4"),
|
||||
UUID: "4",
|
||||
Hostname: "team1host.local",
|
||||
Platform: "darwin",
|
||||
TeamID: &team1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add host to team label
|
||||
err = db.RecordLabelQueryExecutions(context.Background(), h4, map[uint]*bool{teamLabel.ID: ptr.Bool(true)}, time.Now(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global admin can list hosts in team label
|
||||
listHostsInLabelCheckCount(t, db, filter, teamLabel.ID, fleet.HostListOptions{}, 1)
|
||||
|
||||
// Team user can list hosts in their team's label
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleObserver}}}
|
||||
team1Filter := fleet.TeamFilter{User: team1User, IncludeObserver: true}
|
||||
listHostsInLabelCheckCount(t, db, team1Filter, teamLabel.ID, fleet.HostListOptions{}, 1)
|
||||
|
||||
// Trying to list a team label that the user doesn't have access to returns empty
|
||||
team2, err := db.NewTeam(context.Background(), &fleet.Team{Name: "team2_listhosts"})
|
||||
require.NoError(t, err)
|
||||
team2User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleObserver}}}
|
||||
team2Filter := fleet.TeamFilter{User: team2User, IncludeObserver: true}
|
||||
// Team2 user cannot see team1's label, so they get no results
|
||||
team2Hosts, err := db.ListHostsInLabel(context.Background(), team2Filter, teamLabel.ID, fleet.HostListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, team2Hosts) // Returns nil when label is not accessible
|
||||
}
|
||||
|
||||
func listHostsInLabelCheckCount(
|
||||
|
|
@ -705,38 +821,134 @@ func testLabelsGetSpec(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testLabelsApplySpecsRoundtrip(t *testing.T, ds *Datastore) {
|
||||
// TODO test team labels
|
||||
|
||||
expectedSpecs := setupLabelSpecsTest(t, ds)
|
||||
globalSpecs := setupLabelSpecsTest(t, ds)
|
||||
globalOnlyFilter := fleet.TeamFilter{}
|
||||
|
||||
specs, err := ds.GetLabelSpecs(context.Background(), globalOnlyFilter)
|
||||
require.Nil(t, err)
|
||||
test.ElementsMatchSkipTimestampsID(t, expectedSpecs, specs)
|
||||
test.ElementsMatchSkipTimestampsID(t, globalSpecs, specs)
|
||||
|
||||
// Should be idempotent
|
||||
err = ds.ApplyLabelSpecs(context.Background(), expectedSpecs)
|
||||
err = ds.ApplyLabelSpecs(context.Background(), globalSpecs)
|
||||
require.Nil(t, err)
|
||||
specs, err = ds.GetLabelSpecs(context.Background(), globalOnlyFilter)
|
||||
require.Nil(t, err)
|
||||
test.ElementsMatchSkipTimestampsID(t, expectedSpecs, specs)
|
||||
test.ElementsMatchSkipTimestampsID(t, globalSpecs, specs)
|
||||
|
||||
// Test team labels
|
||||
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1_roundtrip"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2_roundtrip"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create team label specs; these wouldn't normally coexist in the same call but that gets handled upstream;
|
||||
// it doesn't hurt anything to set labels cross-team at the data store level (which assumes auth has already
|
||||
// happened).
|
||||
teamSpecs := []*fleet.LabelSpec{
|
||||
{Name: "team1-label", Query: "SELECT 1", TeamID: &team1.ID},
|
||||
{Name: "team2-label", Query: "SELECT 2", TeamID: &team2.ID},
|
||||
}
|
||||
err = ds.ApplyLabelSpecs(context.Background(), teamSpecs)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global filter should still only return global labels
|
||||
specs, err = ds.GetLabelSpecs(context.Background(), globalOnlyFilter)
|
||||
require.NoError(t, err)
|
||||
test.ElementsMatchSkipTimestampsID(t, globalSpecs, specs)
|
||||
|
||||
// Admin user filter should return all labels (global + team)
|
||||
user := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
adminFilter := fleet.TeamFilter{User: user}
|
||||
specs, err = ds.GetLabelSpecs(context.Background(), adminFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, specs, len(globalSpecs)+2)
|
||||
|
||||
// Team1 filter should return only the team1 label
|
||||
team1Filter := fleet.TeamFilter{User: user, TeamID: &team1.ID}
|
||||
specs, err = ds.GetLabelSpecs(context.Background(), team1Filter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, specs, 1)
|
||||
require.Equal(t, "team1-label", specs[0].Name)
|
||||
require.Equal(t, team1.ID, *specs[0].TeamID)
|
||||
|
||||
// Team user can only see their team's labels + global labels with no filter applied
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer}}}
|
||||
team1UserFilter := fleet.TeamFilter{User: team1User}
|
||||
specs, err = ds.GetLabelSpecs(context.Background(), team1UserFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, specs, len(globalSpecs)+1)
|
||||
foundTeam1Label := false
|
||||
for _, s := range specs {
|
||||
if s.Name == "team1-label" {
|
||||
foundTeam1Label = true
|
||||
require.Equal(t, team1.ID, *s.TeamID)
|
||||
}
|
||||
if s.Name == "team2-label" {
|
||||
t.Fatal("team2 label should not be in team1 filter results")
|
||||
}
|
||||
}
|
||||
require.True(t, foundTeam1Label, "team1 label should be found")
|
||||
}
|
||||
|
||||
func testLabelsIDsByName(t *testing.T, ds *Datastore) {
|
||||
setupLabelSpecsTest(t, ds)
|
||||
|
||||
// TODO test team labels
|
||||
|
||||
labels, err := ds.LabelIDsByName(context.Background(), []string{"foo", "bar", "bing"}, fleet.TeamFilter{})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, map[string]uint{"foo": 1, "bar": 2, "bing": 3}, labels)
|
||||
|
||||
// Test team labels
|
||||
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1_idsbyname"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2_idsbyname"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create team labels
|
||||
team1Label, err := ds.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team1-idsbyname",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team2Label, err := ds.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team2-idsbyname",
|
||||
Query: "SELECT 2",
|
||||
TeamID: &team2.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global admin can see all labels
|
||||
adminUser := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
adminFilter := fleet.TeamFilter{User: adminUser}
|
||||
labels, err = ds.LabelIDsByName(context.Background(), []string{"foo", "team1-idsbyname", "team2-idsbyname"}, adminFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 3)
|
||||
assert.Equal(t, team1Label.ID, labels["team1-idsbyname"])
|
||||
assert.Equal(t, team2Label.ID, labels["team2-idsbyname"])
|
||||
|
||||
// Team1 filter should only return global + team1 labels
|
||||
team1Filter := fleet.TeamFilter{User: adminUser, TeamID: &team1.ID}
|
||||
labels, err = ds.LabelIDsByName(context.Background(), []string{"foo", "team1-idsbyname", "team2-idsbyname"}, team1Filter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 2) // foo and team1-idsbyname
|
||||
assert.Equal(t, team1Label.ID, labels["team1-idsbyname"])
|
||||
_, hasTeam2 := labels["team2-idsbyname"]
|
||||
assert.False(t, hasTeam2, "team2 label should not be visible with team1 filter")
|
||||
|
||||
// Team user can only see their team's labels + global labels
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer}}}
|
||||
team1UserFilter := fleet.TeamFilter{User: team1User}
|
||||
labels, err = ds.LabelIDsByName(context.Background(), []string{"foo", "team1-idsbyname", "team2-idsbyname"}, team1UserFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 2) // foo and team1-idsbyname
|
||||
assert.Equal(t, team1Label.ID, labels["team1-idsbyname"])
|
||||
}
|
||||
|
||||
func testLabelsByName(t *testing.T, ds *Datastore) {
|
||||
setupLabelSpecsTest(t, ds)
|
||||
|
||||
// TODO test team labels
|
||||
|
||||
names := []string{"foo", "bar", "bing"}
|
||||
labels, err := ds.LabelsByName(context.Background(), names, fleet.TeamFilter{})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -756,10 +968,146 @@ func testLabelsByName(t *testing.T, ds *Datastore) {
|
|||
assert.Empty(t, labels[name].Description)
|
||||
}
|
||||
}
|
||||
|
||||
// Test team labels
|
||||
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1_byname"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2_byname"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create team labels
|
||||
team1Label, err := ds.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team1-byname",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team2Label, err := ds.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "team2-byname",
|
||||
Query: "SELECT 2",
|
||||
TeamID: &team2.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global admin can see all labels
|
||||
adminUser := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
adminFilter := fleet.TeamFilter{User: adminUser}
|
||||
labelNames := []string{"foo", "team1-byname", "team2-byname"}
|
||||
labels, err = ds.LabelsByName(context.Background(), labelNames, adminFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 3)
|
||||
assert.Equal(t, team1Label.ID, labels["team1-byname"].ID)
|
||||
assert.Equal(t, team2Label.ID, labels["team2-byname"].ID)
|
||||
|
||||
// Team1 filter should only return global + team1 labels
|
||||
team1Filter := fleet.TeamFilter{User: adminUser, TeamID: &team1.ID}
|
||||
labels, err = ds.LabelsByName(context.Background(), labelNames, team1Filter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 2) // foo and team1-byname
|
||||
assert.Equal(t, team1Label.ID, labels["team1-byname"].ID)
|
||||
_, hasTeam2 := labels["team2-byname"]
|
||||
assert.False(t, hasTeam2, "team2 label should not be visible with team1 filter")
|
||||
|
||||
// Team user can only see their team's labels + global labels
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer}}}
|
||||
team1UserFilter := fleet.TeamFilter{User: team1User}
|
||||
labels, err = ds.LabelsByName(context.Background(), labelNames, team1UserFilter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, labels, 2) // foo and team1-byname
|
||||
assert.Equal(t, team1Label.ID, labels["team1-byname"].ID)
|
||||
_, hasTeam2 = labels["team2-byname"]
|
||||
assert.False(t, hasTeam2, "team2 label should not be visible to team1 user")
|
||||
}
|
||||
|
||||
func testLabelByName(t *testing.T, ds *Datastore) {
|
||||
// TODO implement, including team filtering
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: create global labels
|
||||
globalLabel, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "global-label",
|
||||
Query: "SELECT 1",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create teams
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1_labelbyname"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2_labelbyname"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create team labels
|
||||
team1Label, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "team1-labelbyname",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.NewLabel(ctx, &fleet.Label{ // should never be retrieved
|
||||
Name: "team2-labelbyname",
|
||||
Query: "SELECT 2",
|
||||
TeamID: &team2.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
adminUser := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer}}}
|
||||
|
||||
// Global admin can get global label
|
||||
label, err := ds.LabelByName(ctx, "global-label", fleet.TeamFilter{User: adminUser})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, globalLabel.ID, label.ID)
|
||||
|
||||
// Global admin can get team label
|
||||
label, err = ds.LabelByName(ctx, "team1-labelbyname", fleet.TeamFilter{User: adminUser})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, team1Label.ID, label.ID)
|
||||
|
||||
// Team1 user can get global label
|
||||
label, err = ds.LabelByName(ctx, "global-label", fleet.TeamFilter{User: team1User})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, globalLabel.ID, label.ID)
|
||||
|
||||
// Team1 user can get team1 label
|
||||
label, err = ds.LabelByName(ctx, "team1-labelbyname", fleet.TeamFilter{User: team1User})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, team1Label.ID, label.ID)
|
||||
|
||||
// Team1 user cannot get team2 label
|
||||
_, err = ds.LabelByName(ctx, "team2-labelbyname", fleet.TeamFilter{User: team1User})
|
||||
require.Error(t, err)
|
||||
assert.True(t, fleet.IsNotFound(err), "expected not found error for inaccessible team label")
|
||||
|
||||
// Filter to team - team1 filter can see team1 label
|
||||
team1Filter := fleet.TeamFilter{User: adminUser, TeamID: &team1.ID}
|
||||
label, err = ds.LabelByName(ctx, "team1-labelbyname", team1Filter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, team1Label.ID, label.ID)
|
||||
|
||||
// Filter to team - team1 filter cannot see team2 label
|
||||
_, err = ds.LabelByName(ctx, "team2-labelbyname", team1Filter)
|
||||
require.Error(t, err)
|
||||
assert.True(t, fleet.IsNotFound(err))
|
||||
|
||||
// Global-only filter (team_id = 0) can see global label
|
||||
globalOnlyFilter := fleet.TeamFilter{User: adminUser, TeamID: ptr.Uint(0)}
|
||||
label, err = ds.LabelByName(ctx, "global-label", globalOnlyFilter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, globalLabel.ID, label.ID)
|
||||
|
||||
// Global-only filter cannot see team label
|
||||
_, err = ds.LabelByName(ctx, "team1-labelbyname", globalOnlyFilter)
|
||||
require.Error(t, err)
|
||||
assert.True(t, fleet.IsNotFound(err))
|
||||
|
||||
// Non-existent label returns not found
|
||||
_, err = ds.LabelByName(ctx, "nonexistent-label", fleet.TeamFilter{User: adminUser})
|
||||
require.Error(t, err)
|
||||
assert.True(t, fleet.IsNotFound(err))
|
||||
}
|
||||
|
||||
func testLabelsSave(t *testing.T, db *Datastore) {
|
||||
|
|
@ -893,8 +1241,6 @@ func testLabelsRecordNonexistentQueryLabelExecution(t *testing.T, db *Datastore)
|
|||
}
|
||||
|
||||
func testDeleteLabel(t *testing.T, db *Datastore) {
|
||||
// TODO test team label filtering
|
||||
|
||||
ctx := context.Background()
|
||||
l, err := db.NewLabel(ctx, &fleet.Label{
|
||||
Name: t.Name(),
|
||||
|
|
@ -954,6 +1300,52 @@ func testDeleteLabel(t *testing.T, db *Datastore) {
|
|||
err = db.DeleteLabel(ctx, l2.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsForeignKey(err))
|
||||
|
||||
// Test team label filtering
|
||||
team1, err := db.NewTeam(ctx, &fleet.Team{Name: "team1_delete"})
|
||||
require.NoError(t, err)
|
||||
team2, err := db.NewTeam(ctx, &fleet.Team{Name: "team2_delete"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create team labels
|
||||
team1Label, err := db.NewLabel(ctx, &fleet.Label{
|
||||
Name: "team1-delete-label",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team2Label, err := db.NewLabel(ctx, &fleet.Label{
|
||||
Name: "team2-delete-label",
|
||||
Query: "SELECT 2",
|
||||
TeamID: &team2.ID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
adminUser := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
|
||||
team1User := &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer}}}
|
||||
|
||||
// Global admin can delete team labels
|
||||
err = db.DeleteLabel(ctx, team1Label.Name, fleet.TeamFilter{User: adminUser})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify it's deleted
|
||||
_, err = db.LabelByName(ctx, team1Label.Name, fleet.TeamFilter{User: adminUser})
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
|
||||
// Team user cannot delete label from another team (not found because not visible)
|
||||
err = db.DeleteLabel(ctx, team2Label.Name, fleet.TeamFilter{User: team1User})
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
|
||||
// Verify team2 label still exists
|
||||
label, err := db.LabelByName(ctx, team2Label.Name, fleet.TeamFilter{User: adminUser})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, team2Label.ID, label.ID)
|
||||
|
||||
// Admin with team filter can delete
|
||||
err = db.DeleteLabel(ctx, team2Label.Name, fleet.TeamFilter{User: adminUser, TeamID: &team2.ID})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testLabelsSummaryAndListTeamFiltering(t *testing.T, db *Datastore) {
|
||||
|
|
@ -1861,8 +2253,6 @@ func labelIDFromName(t *testing.T, ds fleet.Datastore, name string) uint {
|
|||
}
|
||||
|
||||
func testUpdateLabelMembershipByHostIDs(t *testing.T, ds *Datastore) {
|
||||
// TODO validate team label host validation behavior
|
||||
|
||||
ctx := context.Background()
|
||||
filter := fleet.TeamFilter{User: test.UserAdmin}
|
||||
|
||||
|
|
@ -2027,6 +2417,86 @@ func testUpdateLabelMembershipByHostIDs(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, strconv.Itoa(int(host1.ID)), labelSpec.Hosts[0]) //nolint:gosec // dismiss G115
|
||||
require.Equal(t, strconv.Itoa(int(host2.ID)), labelSpec.Hosts[1]) //nolint:gosec // dismiss G115
|
||||
require.Equal(t, strconv.Itoa(int(host3.ID)), labelSpec.Hosts[2]) //nolint:gosec // dismiss G115
|
||||
|
||||
// Test team label host validation behavior
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1_membership"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2_membership"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create hosts on different teams
|
||||
hostTeam1, err := ds.NewHost(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("team1host"),
|
||||
NodeKey: ptr.String("team1host"),
|
||||
UUID: "team1host",
|
||||
Hostname: "team1host.local",
|
||||
Platform: "darwin",
|
||||
TeamID: &team1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hostTeam2, err := ds.NewHost(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("team2host"),
|
||||
NodeKey: ptr.String("team2host"),
|
||||
UUID: "team2host",
|
||||
Hostname: "team2host.local",
|
||||
Platform: "darwin",
|
||||
TeamID: &team2.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hostNoTeam, err := ds.NewHost(ctx, &fleet.Host{
|
||||
OsqueryHostID: ptr.String("noteamhost"),
|
||||
NodeKey: ptr.String("noteamhost"),
|
||||
UUID: "noteamhost",
|
||||
Hostname: "noteamhost.local",
|
||||
Platform: "darwin",
|
||||
TeamID: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a team label
|
||||
teamLabel, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "team1-membership-label",
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
TeamID: &team1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Adding a team1 host to a team1 label should succeed
|
||||
teamLabelResult, teamHostIDs, err := ds.UpdateLabelMembershipByHostIDs(ctx, *teamLabel, []uint{hostTeam1.ID}, filter)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, teamLabelResult.HostCount)
|
||||
require.Len(t, teamHostIDs, 1)
|
||||
require.Equal(t, hostTeam1.ID, teamHostIDs[0])
|
||||
|
||||
// Adding a team2 host to a team1 label should fail
|
||||
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *teamLabel, []uint{hostTeam2.ID}, filter)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "supplied hosts are on a different team than the label")
|
||||
|
||||
// Adding a no-team host to a team1 label should fail
|
||||
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *teamLabel, []uint{hostNoTeam.ID}, filter)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "supplied hosts are on a different team than the label")
|
||||
|
||||
// Adding mixed hosts (team1 + team2) to a team1 label should fail
|
||||
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *teamLabel, []uint{hostTeam1.ID, hostTeam2.ID}, filter)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "supplied hosts are on a different team than the label")
|
||||
|
||||
// Global label can have hosts from any team
|
||||
globalMembershipLabel, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "global-membership-label",
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
TeamID: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
globalLabelResult, globalHostIDs, err := ds.UpdateLabelMembershipByHostIDs(ctx, *globalMembershipLabel, []uint{hostTeam1.ID, hostTeam2.ID, hostNoTeam.ID}, filter)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, globalLabelResult.HostCount)
|
||||
require.Len(t, globalHostIDs, 3)
|
||||
}
|
||||
|
||||
func testApplyLabelSpecsForSerialUUID(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -2585,5 +3055,239 @@ func testUpdateLabelMembershipForTransferredHost(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
func testSetAsideLabels(t *testing.T, ds *Datastore) {
|
||||
// TODO
|
||||
ctx := context.Background()
|
||||
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1_setaside"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2_setaside"})
|
||||
require.NoError(t, err)
|
||||
|
||||
i := 0
|
||||
newUser := func(u fleet.User) *fleet.User {
|
||||
i++
|
||||
u.Name = fmt.Sprintf("SetAsideUser%d", i)
|
||||
u.Email = fmt.Sprintf("%s@example.com", u.Name)
|
||||
u.Password = []byte("foobar")
|
||||
persisted, err := ds.NewUser(ctx, &u)
|
||||
require.NoError(t, err)
|
||||
return persisted
|
||||
}
|
||||
|
||||
// Create users that are reused across tests
|
||||
globalAdmin := newUser(fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)})
|
||||
team1Admin := newUser(fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleAdmin}}})
|
||||
team1Maintainer := newUser(fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer}}})
|
||||
multiTeamUser := newUser(fleet.User{Teams: []fleet.UserTeam{
|
||||
{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer},
|
||||
{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleMaintainer},
|
||||
}})
|
||||
|
||||
type labelSpec struct {
|
||||
name string
|
||||
teamID *uint
|
||||
authorID *uint
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
labels []labelSpec // labels to create for this test
|
||||
notOnTeamID *uint // team ID being applied to
|
||||
labelNames []string // names to pass to SetAsideLabels (if nil, uses labels[*].name)
|
||||
user *fleet.User
|
||||
expectError bool
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "empty names list is a no-op",
|
||||
labels: nil,
|
||||
notOnTeamID: nil,
|
||||
labelNames: []string{},
|
||||
user: globalAdmin,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "global admin can set aside global labels when applying to a team",
|
||||
labels: []labelSpec{{name: "global-setaside-1", teamID: nil, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: globalAdmin,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "global maintainer can set aside global labels",
|
||||
labels: []labelSpec{{name: "global-setaside-2", teamID: nil, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: newUser(fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "global gitops can set aside global labels",
|
||||
labels: []labelSpec{{name: "global-setaside-3", teamID: nil, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: newUser(fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "global observer cannot set aside global labels",
|
||||
labels: []labelSpec{{name: "global-setaside-4", teamID: nil, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: newUser(fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "team admin can set aside their own team's labels",
|
||||
labels: []labelSpec{{name: "team1-setaside-1", teamID: &team1.ID, authorID: nil}},
|
||||
notOnTeamID: &team2.ID,
|
||||
user: team1Admin,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "team maintainer can set aside their own team's labels",
|
||||
labels: []labelSpec{{name: "team1-setaside-2", teamID: &team1.ID, authorID: nil}},
|
||||
notOnTeamID: &team2.ID,
|
||||
user: team1Maintainer,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "team gitops can set aside their own team's labels",
|
||||
labels: []labelSpec{{name: "team1-setaside-3", teamID: &team1.ID, authorID: nil}},
|
||||
notOnTeamID: &team2.ID,
|
||||
user: newUser(fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleGitOps}}}),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "team observer cannot set aside team labels",
|
||||
labels: []labelSpec{{name: "team1-setaside-4", teamID: &team1.ID, authorID: nil}},
|
||||
notOnTeamID: &team2.ID,
|
||||
user: newUser(fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleObserver}}}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "cannot set aside labels from the same team we're applying to",
|
||||
labels: []labelSpec{{name: "team1-setaside-5", teamID: &team1.ID, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: team1Admin,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "team admin cannot set aside labels when applying to the same team, even if that user authored",
|
||||
labels: []labelSpec{{name: "team1-setaside-6", teamID: &team1.ID, authorID: &team1Admin.ID}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: team1Admin,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "cannot set aside global labels when applying to global",
|
||||
labels: []labelSpec{{name: "global-setaside-5", teamID: nil, authorID: nil}},
|
||||
notOnTeamID: nil,
|
||||
user: globalAdmin,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "team user with write role can set aside their authored global labels",
|
||||
labels: []labelSpec{{name: "global-authored-setaside", teamID: nil, authorID: &team1Maintainer.ID}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: team1Maintainer,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "team user cannot set aside non-authored global labels",
|
||||
labels: []labelSpec{{name: "global-nonauthored-setaside", teamID: nil, authorID: &globalAdmin.ID}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: team1Maintainer,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "multi-team user can set aside labels from teams they have write access to",
|
||||
labels: []labelSpec{{name: "team2-setaside-1", teamID: &team2.ID, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: newUser(fleet.User{Teams: []fleet.UserTeam{
|
||||
{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer},
|
||||
{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleAdmin},
|
||||
}}),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multi-team user cannot set aside labels from teams they don't have write access to",
|
||||
labels: []labelSpec{{name: "team2-setaside-2", teamID: &team2.ID, authorID: nil}},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: newUser(fleet.User{Teams: []fleet.UserTeam{
|
||||
{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleMaintainer},
|
||||
{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleObserver},
|
||||
}}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent label should fail",
|
||||
labels: nil,
|
||||
notOnTeamID: &team1.ID,
|
||||
labelNames: []string{"nonexistent-label"},
|
||||
user: globalAdmin,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "multiple labels can be set aside at once",
|
||||
labels: []labelSpec{
|
||||
{name: "multi-setaside-1", teamID: nil, authorID: &multiTeamUser.ID},
|
||||
{name: "multi-setaside-2", teamID: &team2.ID, authorID: nil},
|
||||
},
|
||||
notOnTeamID: &team1.ID,
|
||||
user: multiTeamUser,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create labels for this test case
|
||||
var expectedLabelNames []string
|
||||
for _, spec := range tc.labels {
|
||||
// Build expected renamed name based on label's team ID
|
||||
var expectedSuffix string
|
||||
if spec.teamID == nil {
|
||||
expectedSuffix = "__team_0"
|
||||
} else {
|
||||
expectedSuffix = fmt.Sprintf("__team_%d", *spec.teamID)
|
||||
}
|
||||
expectedLabelNames = append(expectedLabelNames, spec.name+expectedSuffix)
|
||||
|
||||
_, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: spec.name,
|
||||
Query: "SELECT 1",
|
||||
TeamID: spec.teamID,
|
||||
AuthorID: spec.authorID,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Determine label names to use, and which to expect
|
||||
labelNames := tc.labelNames
|
||||
if labelNames == nil {
|
||||
for _, spec := range tc.labels {
|
||||
labelNames = append(labelNames, spec.name)
|
||||
}
|
||||
}
|
||||
|
||||
err := ds.SetAsideLabels(ctx, tc.notOnTeamID, labelNames, *tc.user)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// Original name should not exist
|
||||
oldLabels, _ := ds.LabelsByName(ctx, labelNames, fleet.TeamFilter{User: globalAdmin})
|
||||
require.Empty(t, oldLabels)
|
||||
|
||||
// All renamed labels should exist
|
||||
renamedLabels, _ := ds.LabelsByName(ctx, expectedLabelNames, fleet.TeamFilter{User: globalAdmin})
|
||||
require.Len(t, renamedLabels, len(expectedLabelNames))
|
||||
for _, expected := range expectedLabelNames {
|
||||
_, ok := renamedLabels[expected]
|
||||
require.Truef(t, ok, "Missing renamed label %s", expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3203,7 +3203,7 @@ func (svc *Service) validateLabelNames(ctx context.Context, action string, label
|
|||
})
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf(
|
||||
"Couldn't %s labels. Labels not found: %s. All labels must exist.",
|
||||
"Couldn't %s labels. Labels not found: %s. All labels must exist and be either global or on the same team as the host.",
|
||||
action,
|
||||
strings.Join(labelsNotFound, ", "),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4563,8 +4563,6 @@ func (s *integrationTestSuite) TestGetMacadminsData() {
|
|||
}
|
||||
|
||||
func (s *integrationTestSuite) TestLabels() {
|
||||
// TODO team labels
|
||||
|
||||
t := s.T()
|
||||
|
||||
// create some hosts to use for manual labels
|
||||
|
|
@ -5382,8 +5380,6 @@ func (s *integrationTestSuite) TestLabelSpecs() {
|
|||
|
||||
// get a non-existing label spec
|
||||
s.DoJSON("GET", "/api/latest/fleet/spec/labels/zzz", nil, http.StatusNotFound, &getResp)
|
||||
|
||||
// TODO team labels
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestUsers() {
|
||||
|
|
@ -13479,8 +13475,6 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() {
|
|||
}
|
||||
|
||||
func (s *integrationTestSuite) TestAddingRemovingManualLabels() {
|
||||
// TODO team labels
|
||||
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
@ -13610,13 +13604,13 @@ func (s *integrationTestSuite) TestAddingRemovingManualLabels() {
|
|||
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.")
|
||||
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.")
|
||||
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},
|
||||
|
|
@ -13641,13 +13635,13 @@ func (s *integrationTestSuite) TestAddingRemovingManualLabels() {
|
|||
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.")
|
||||
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.")
|
||||
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"},
|
||||
|
|
@ -13778,6 +13772,7 @@ func (s *integrationTestSuite) TestAddingRemovingManualLabels() {
|
|||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
teamHost2Labels = getHostLabels(teamHost2)
|
||||
require.Empty(t, teamHost2Labels)
|
||||
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestDebugDB() {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
|
@ -855,6 +856,282 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecsPermissions() {
|
|||
s.Do("POST", "/api/latest/fleet/spec/teams", editTeam2Spec, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestTeamLabels() {
|
||||
t := s.T()
|
||||
|
||||
// Create teams for testing team labels
|
||||
team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: t.Name() + "_team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: t.Name() + "_team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create hosts on different teams
|
||||
teamHosts := s.createHosts(t, "darwin", "darwin")
|
||||
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&team1.ID, []uint{teamHosts[0].ID})))
|
||||
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&team2.ID, []uint{teamHosts[1].ID})))
|
||||
|
||||
// Create team labels using datastore directly (LabelPayload doesn't support TeamID)
|
||||
team1Label, err := s.ds.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: t.Name() + "_team1_label",
|
||||
Query: "SELECT 1",
|
||||
TeamID: &team1.ID,
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team1LabelID := team1Label.ID
|
||||
|
||||
// Create a team2 label using datastore
|
||||
team2Label, err := s.ds.NewLabel(context.Background(), &fleet.Label{
|
||||
Name: "__team2_label",
|
||||
Query: "SELECT 2",
|
||||
TeamID: &team2.ID,
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team2LabelID := team2Label.ID
|
||||
|
||||
// Get the team label via API
|
||||
var getResp getLabelResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d", team1LabelID), nil, http.StatusOK, &getResp)
|
||||
require.Equal(t, team1LabelID, getResp.Label.ID)
|
||||
require.NotNil(t, getResp.Label.TeamID)
|
||||
require.Equal(t, team1.ID, *getResp.Label.TeamID)
|
||||
|
||||
// List labels should include team labels for admin
|
||||
var listResp listLabelsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp)
|
||||
foundTeamLabel := false
|
||||
foundTeam2Label := false
|
||||
for _, lbl := range listResp.Labels {
|
||||
if lbl.ID == team1LabelID {
|
||||
foundTeamLabel = true
|
||||
require.NotNil(t, lbl.TeamID)
|
||||
require.Equal(t, team1.ID, *lbl.TeamID)
|
||||
}
|
||||
if lbl.ID == team2LabelID {
|
||||
foundTeam2Label = true
|
||||
require.NotNil(t, lbl.TeamID)
|
||||
require.Equal(t, team2.ID, *lbl.TeamID)
|
||||
}
|
||||
}
|
||||
require.True(t, foundTeamLabel, "team label should be in list")
|
||||
require.True(t, foundTeam2Label, "team 2 label should be in list")
|
||||
|
||||
// Filter to specific team
|
||||
listResp = listLabelsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "team_id", fmt.Sprint(team1.ID))
|
||||
foundTeamLabel = false
|
||||
for _, lbl := range listResp.Labels {
|
||||
if lbl.ID == team1LabelID {
|
||||
foundTeamLabel = true
|
||||
}
|
||||
// Should not find team2 labels when filtering to team1
|
||||
if lbl.TeamID != nil && *lbl.TeamID == team2.ID {
|
||||
t.Fatal("should not find team2 labels when filtering to team1")
|
||||
}
|
||||
}
|
||||
require.True(t, foundTeamLabel, "team1 label should be in filtered list")
|
||||
|
||||
// Filter to global only (team_id=0) should not include team labels
|
||||
listResp = listLabelsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "team_id", "0")
|
||||
for _, lbl := range listResp.Labels {
|
||||
require.Nil(t, lbl.TeamID, "global-only filter should not return team labels")
|
||||
}
|
||||
|
||||
// Modify team label
|
||||
var modResp modifyLabelResponse
|
||||
newName := t.Name() + "_team1_label_modified"
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", team1LabelID), &fleet.ModifyLabelPayload{
|
||||
Name: &newName,
|
||||
}, http.StatusOK, &modResp)
|
||||
require.Equal(t, newName, modResp.Label.Name)
|
||||
|
||||
// Delete team labels
|
||||
var delResp deleteLabelResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/id/%d", team1LabelID), nil, http.StatusOK, &delResp)
|
||||
|
||||
// Verify it's deleted
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d", team1LabelID), nil, http.StatusNotFound, &getResp)
|
||||
|
||||
// Delete team2 label by name
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/%s", team2Label.Name), nil, http.StatusOK, &delResp)
|
||||
|
||||
// Verify it's deleted
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d", team2LabelID), nil, http.StatusNotFound, &getResp)
|
||||
|
||||
/////// LABEL SPECS
|
||||
|
||||
// Apply a team label spec
|
||||
teamLabelName := strings.ReplaceAll(t.Name(), "/", "_") + "_team1_label"
|
||||
var applyResp applyLabelSpecsResponse
|
||||
// make sure we fail loudly when we spec the request wrong
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{
|
||||
Specs: []*fleet.LabelSpec{
|
||||
{
|
||||
Name: teamLabelName,
|
||||
Query: "select 1",
|
||||
Platform: "darwin",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
TeamID: &team1.ID,
|
||||
},
|
||||
},
|
||||
}, http.StatusUnprocessableEntity, &applyResp)
|
||||
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/spec/labels?team_id=%d", team1.ID), applyLabelSpecsRequest{
|
||||
Specs: []*fleet.LabelSpec{
|
||||
{
|
||||
Name: teamLabelName,
|
||||
Query: "select 1",
|
||||
Platform: "darwin",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
|
||||
},
|
||||
},
|
||||
}, http.StatusOK, &applyResp)
|
||||
|
||||
// List label specs should include team label for admin
|
||||
var specsResp getLabelSpecsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/spec/labels", nil, http.StatusOK, &specsResp)
|
||||
foundTeamLabel = false
|
||||
|
||||
for _, spec := range specsResp.Specs {
|
||||
if spec.Name == teamLabelName {
|
||||
foundTeamLabel = true
|
||||
require.NotNil(t, spec.TeamID)
|
||||
require.Equal(t, team1.ID, *spec.TeamID)
|
||||
}
|
||||
}
|
||||
require.True(t, foundTeamLabel, "team label spec should be in list")
|
||||
|
||||
// Get the specific team label spec
|
||||
var getSpecResp getLabelSpecResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/spec/labels/%s", url.PathEscape(teamLabelName)), nil, http.StatusOK, &getSpecResp)
|
||||
require.Equal(t, teamLabelName, getSpecResp.Spec.Name)
|
||||
require.NotNil(t, getSpecResp.Spec.TeamID)
|
||||
require.Equal(t, team1.ID, *getSpecResp.Spec.TeamID)
|
||||
|
||||
// Filter specs to specific team
|
||||
s.DoJSON("GET", "/api/latest/fleet/spec/labels", nil, http.StatusOK, &specsResp, "team_id", fmt.Sprint(team1.ID))
|
||||
foundTeamLabel = false
|
||||
for _, spec := range specsResp.Specs {
|
||||
if spec.Name == teamLabelName {
|
||||
foundTeamLabel = true
|
||||
}
|
||||
// Team2 labels should not be present
|
||||
if spec.TeamID != nil && *spec.TeamID == team2.ID {
|
||||
t.Fatal("should not find team2 labels when filtering to team1")
|
||||
}
|
||||
}
|
||||
require.True(t, foundTeamLabel, "team1 label spec should be in filtered list")
|
||||
|
||||
// Filter specs to global only (team_id=0)
|
||||
s.DoJSON("GET", "/api/latest/fleet/spec/labels", nil, http.StatusOK, &specsResp, "team_id", "0")
|
||||
for _, spec := range specsResp.Specs {
|
||||
require.Nil(t, spec.TeamID, "global-only filter should not return team labels")
|
||||
}
|
||||
|
||||
/////// TEAM MANUAL LABELS RESTRICTIONS
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a global host for testing restrictions
|
||||
globalHost, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String(t.Name() + "_global_host"),
|
||||
NodeKey: ptr.String(t.Name() + "_global_host"),
|
||||
UUID: t.Name() + "_global_host",
|
||||
Hostname: t.Name() + "_global_host.local",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a team admin user for team1
|
||||
teamAdminEmail := t.Name() + "_team_admin@example.com"
|
||||
teamAdmin := &fleet.User{
|
||||
Name: teamAdminEmail,
|
||||
Email: teamAdminEmail,
|
||||
Teams: []fleet.UserTeam{
|
||||
{
|
||||
Team: *team1,
|
||||
Role: fleet.RoleAdmin,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, teamAdmin.SetPassword(test.GoodPassword, 10, 10))
|
||||
teamAdmin, err = s.ds.NewUser(ctx, teamAdmin)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a team label (manual) on team1
|
||||
teamManualLabel, err := s.ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: t.Name() + "_teamManualLabel1",
|
||||
LabelMembershipType: fleet.LabelMembershipTypeManual,
|
||||
TeamID: &team1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Helper function to get host label names
|
||||
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)
|
||||
labels := make([]string, 0, len(hostResp.Host.Labels))
|
||||
for _, lbl := range hostResp.Host.Labels {
|
||||
labels = append(labels, lbl.Name)
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
// Admin can add team label to team host (teamHosts[0] is on team1)
|
||||
var addLabelsToHostResp addLabelsToHostResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHosts[0].ID), addLabelsToHostRequest{
|
||||
Labels: []string{teamManualLabel.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
team1HostLabels := getHostLabels(teamHosts[0])
|
||||
require.Contains(t, team1HostLabels, teamManualLabel.Name)
|
||||
|
||||
// Cannot add team1 label to team2 host (teamHosts[1] is on team2)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHosts[1].ID), addLabelsToHostRequest{
|
||||
Labels: []string{teamManualLabel.Name},
|
||||
}, http.StatusBadRequest, &addLabelsToHostResp)
|
||||
|
||||
// Cannot add team1 label to global host
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", globalHost.ID), addLabelsToHostRequest{
|
||||
Labels: []string{teamManualLabel.Name},
|
||||
}, http.StatusBadRequest, &addLabelsToHostResp)
|
||||
|
||||
// Team admin can add their team's label to their team's host
|
||||
// First remove the label added by admin
|
||||
var removeLabelsFromHostResp removeLabelsFromHostResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHosts[0].ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{teamManualLabel.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
|
||||
s.token = s.getTestToken(teamAdmin.Email, test.GoodPassword)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHosts[0].ID), addLabelsToHostRequest{
|
||||
Labels: []string{teamManualLabel.Name},
|
||||
}, http.StatusOK, &addLabelsToHostResp)
|
||||
team1HostLabels = getHostLabels(teamHosts[0])
|
||||
require.Contains(t, team1HostLabels, teamManualLabel.Name)
|
||||
|
||||
// Team admin can remove their team's label from their team's host
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/labels", teamHosts[0].ID), removeLabelsFromHostRequest{
|
||||
Labels: []string{teamManualLabel.Name},
|
||||
}, http.StatusOK, &removeLabelsFromHostResp)
|
||||
team1HostLabels = getHostLabels(teamHosts[0])
|
||||
require.NotContains(t, team1HostLabels, teamManualLabel.Name)
|
||||
|
||||
// Reset token to admin
|
||||
s.token = s.getTestAdminToken()
|
||||
|
||||
// Clean up teams
|
||||
require.NoError(t, s.ds.DeleteTeam(context.Background(), team1.ID))
|
||||
require.NoError(t, s.ds.DeleteTeam(context.Background(), team2.ID))
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestTeamSchedule() {
|
||||
t := s.T()
|
||||
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ func getTeamIDOrZeroForGlobal(stringID *string) *uint {
|
|||
}
|
||||
|
||||
func (svc *Service) ListLabels(ctx context.Context, opt fleet.ListOptions, teamID *uint, includeHostCounts bool) ([]*fleet.Label, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{}, fleet.ActionRead); err != nil {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -352,6 +352,10 @@ func (svc *Service) ListLabels(ctx context.Context, opt fleet.ListOptions, teamI
|
|||
return nil, fleet.ErrNoContext
|
||||
}
|
||||
|
||||
if !license.IsPremium(ctx) && teamID != nil && *teamID > 0 {
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
// TODO(mna): ListLabels doesn't currently return the hostIDs members of the
|
||||
// label, the quick approach would be an N+1 queries endpoint. Leaving like
|
||||
// that for now because we're in a hurry before merge freeze but the solution
|
||||
|
|
@ -405,7 +409,7 @@ func getLabelsSummaryEndpoint(ctx context.Context, request interface{}, svc flee
|
|||
}
|
||||
|
||||
func (svc *Service) LabelsSummary(ctx context.Context, teamID *uint) ([]*fleet.LabelSummary, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{}, fleet.ActionRead); err != nil {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -414,6 +418,10 @@ func (svc *Service) LabelsSummary(ctx context.Context, teamID *uint) ([]*fleet.L
|
|||
return nil, fleet.ErrNoContext
|
||||
}
|
||||
|
||||
if !license.IsPremium(ctx) && teamID != nil && *teamID > 0 {
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
return svc.ds.LabelsSummary(ctx, fleet.TeamFilter{User: vc.User, IncludeObserver: true, TeamID: teamID})
|
||||
}
|
||||
|
||||
|
|
@ -682,6 +690,13 @@ func (svc *Service) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpe
|
|||
}
|
||||
|
||||
// make sure we're only upserting labels on the team we specified; individual spec teams aren't used on writes
|
||||
if spec.TeamID != nil {
|
||||
return fleet.NewUserMessageError(
|
||||
ctxerr.New(
|
||||
ctx,
|
||||
"When applying team label specs, provide the team label by URL query string parameter rather than within the JSON request body",
|
||||
), http.StatusUnprocessableEntity)
|
||||
}
|
||||
spec.TeamID = teamID
|
||||
regularSpecs = append(regularSpecs, spec)
|
||||
}
|
||||
|
|
@ -749,10 +764,14 @@ func getLabelSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S
|
|||
}
|
||||
|
||||
func (svc *Service) GetLabelSpecs(ctx context.Context, teamID *uint) ([]*fleet.LabelSpec, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{}, fleet.ActionRead); err != nil {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !license.IsPremium(ctx) && teamID != nil && *teamID > 0 {
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fleet.ErrNoContext
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql/testing_utils"
|
||||
|
|
@ -41,18 +42,40 @@ func TestLabelsAuth(t *testing.T) {
|
|||
ds.ApplyLabelSpecsFunc = func(ctx context.Context, specs []*fleet.LabelSpec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
team1ID := uint(1)
|
||||
team2ID := uint(2)
|
||||
|
||||
team1LabelID := uint(3)
|
||||
team2LabelID := uint(4)
|
||||
|
||||
team1Label := fleet.Label{ID: team1LabelID, Name: "team1-label", TeamID: &team1ID}
|
||||
team2Label := fleet.Label{ID: team2LabelID, Name: "team2-label", TeamID: &team2ID}
|
||||
|
||||
// Update LabelFunc to handle team labels
|
||||
ds.LabelFunc = func(ctx context.Context, id uint, filter fleet.TeamFilter) (*fleet.LabelWithTeamName, []uint, error) {
|
||||
switch id {
|
||||
case uint(1):
|
||||
return &fleet.LabelWithTeamName{Label: fleet.Label{ID: id, AuthorID: &filter.User.ID}}, nil, nil
|
||||
case uint(2):
|
||||
return &fleet.LabelWithTeamName{Label: fleet.Label{ID: id}}, nil, nil
|
||||
case team1LabelID: // team1 label
|
||||
return &fleet.LabelWithTeamName{Label: team1Label}, nil, nil
|
||||
case team2LabelID: // team2 label
|
||||
return &fleet.LabelWithTeamName{Label: team2Label}, nil, nil
|
||||
}
|
||||
|
||||
return nil, nil, ctxerr.Wrap(ctx, notFoundErr{"label", fleet.ErrorWithUUID{}})
|
||||
}
|
||||
|
||||
ds.LabelByNameFunc = func(ctx context.Context, name string, filter fleet.TeamFilter) (*fleet.Label, error) {
|
||||
return &fleet.Label{ID: 2, Name: name}, nil // for deletes, TODO add cases for authorship/team differences
|
||||
switch name {
|
||||
case "team1-label":
|
||||
return &team1Label, nil
|
||||
case "team2-label":
|
||||
return &team2Label, nil
|
||||
default:
|
||||
return &fleet.Label{ID: 2, Name: name}, nil
|
||||
}
|
||||
}
|
||||
ds.ListLabelsFunc = func(ctx context.Context, filter fleet.TeamFilter, opts fleet.ListOptions, includeHostCounts bool) ([]*fleet.Label, error) {
|
||||
return nil, nil
|
||||
|
|
@ -76,6 +99,10 @@ func TestLabelsAuth(t *testing.T) {
|
|||
shouldFailGlobalWrite bool
|
||||
shouldFailGlobalRead bool
|
||||
shouldFailGlobalWriteIfAuthor bool
|
||||
shouldFailTeam1Write bool
|
||||
shouldFailTeam1Read bool
|
||||
shouldFailTeam2Write bool
|
||||
shouldFailTeam2Read bool
|
||||
}{
|
||||
{
|
||||
"global admin",
|
||||
|
|
@ -83,6 +110,10 @@ func TestLabelsAuth(t *testing.T) {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global maintainer",
|
||||
|
|
@ -90,6 +121,10 @@ func TestLabelsAuth(t *testing.T) {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global observer",
|
||||
|
|
@ -97,20 +132,32 @@ func TestLabelsAuth(t *testing.T) {
|
|||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"team maintainer",
|
||||
"team 1 maintainer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"team observer",
|
||||
"team 1 observer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -118,8 +165,6 @@ func TestLabelsAuth(t *testing.T) {
|
|||
otherLabel, _, err := svc.NewLabel(viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleMaintainer)}}), fleet.LabelPayload{Name: "Other label", Query: "SELECT 0"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// TODO create other-team label
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
||||
|
|
@ -127,47 +172,89 @@ func TestLabelsAuth(t *testing.T) {
|
|||
myLabel, _, err := svc.NewLabel(ctx, fleet.LabelPayload{Name: t.Name(), Query: `SELECT 1`})
|
||||
checkAuthErr(t, tt.shouldFailGlobalWriteIfAuthor, err) // team write users can still create global labels
|
||||
|
||||
_, _, err = svc.ModifyLabel(ctx, otherLabel.ID, fleet.ModifyLabelPayload{})
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
|
||||
if myLabel != nil {
|
||||
_, _, err = svc.ModifyLabel(ctx, myLabel.ID, fleet.ModifyLabelPayload{})
|
||||
checkAuthErr(t, tt.shouldFailGlobalWriteIfAuthor, err)
|
||||
}
|
||||
|
||||
err = svc.ApplyLabelSpecs(ctx, []*fleet.LabelSpec{}, nil, nil)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
|
||||
_, _, err = svc.GetLabel(ctx, otherLabel.ID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.GetLabelSpecs(ctx, nil)
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.GetLabelSpec(ctx, "abc")
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.ListLabels(ctx, fleet.ListOptions{}, nil, true)
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.LabelsSummary(ctx, nil)
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.ListHostsInLabel(ctx, 1, fleet.HostListOptions{})
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
err = svc.DeleteLabel(ctx, "abc")
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
|
||||
err = svc.DeleteLabelByID(ctx, otherLabel.ID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
|
||||
if myLabel != nil {
|
||||
err = svc.DeleteLabelByID(ctx, myLabel.ID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWriteIfAuthor, err)
|
||||
}
|
||||
|
||||
// TODO add team label permissions
|
||||
for _, tc := range []struct {
|
||||
label fleet.Label
|
||||
shouldFailWrite bool
|
||||
shouldFailRead bool
|
||||
}{
|
||||
{*otherLabel, tt.shouldFailGlobalWrite, tt.shouldFailGlobalRead},
|
||||
{team1Label, tt.shouldFailTeam1Write, tt.shouldFailTeam1Read},
|
||||
{team2Label, tt.shouldFailTeam2Write, tt.shouldFailTeam2Read},
|
||||
} {
|
||||
t.Run(tc.label.Name, func(t *testing.T) {
|
||||
ctx2 := ctx
|
||||
if tc.label.TeamID != nil {
|
||||
// license check is after auth check
|
||||
if !tc.shouldFailRead {
|
||||
_, err = svc.GetLabelSpecs(ctx2, tc.label.TeamID)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
|
||||
_, err = svc.ListLabels(ctx2, fleet.ListOptions{}, tc.label.TeamID, true)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
|
||||
_, err = svc.LabelsSummary(ctx2, tc.label.TeamID)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
}
|
||||
if !tc.shouldFailWrite {
|
||||
require.ErrorIs(t, svc.ApplyLabelSpecs(ctx2, []*fleet.LabelSpec{}, tc.label.TeamID, nil), fleet.ErrMissingLicense)
|
||||
|
||||
// We'll let global admins clean up team labels if they downgraded to Free
|
||||
require.NoError(t, svc.DeleteLabel(ctx2, tc.label.Name))
|
||||
require.NoError(t, svc.DeleteLabelByID(ctx2, tc.label.ID))
|
||||
}
|
||||
|
||||
ctx2 = license.NewContext(ctx2, &fleet.LicenseInfo{Tier: fleet.TierPremium})
|
||||
}
|
||||
|
||||
err = svc.ApplyLabelSpecs(ctx2, []*fleet.LabelSpec{}, tc.label.TeamID, nil)
|
||||
checkAuthErr(t, tc.shouldFailWrite, err)
|
||||
|
||||
_, err = svc.GetLabelSpecs(ctx2, tc.label.TeamID)
|
||||
checkAuthErr(t, tc.shouldFailRead, err)
|
||||
|
||||
_, err = svc.ListLabels(ctx2, fleet.ListOptions{}, tc.label.TeamID, true)
|
||||
checkAuthErr(t, tc.shouldFailRead, err)
|
||||
|
||||
_, err = svc.LabelsSummary(ctx2, tc.label.TeamID)
|
||||
checkAuthErr(t, tc.shouldFailRead, err)
|
||||
|
||||
err = svc.DeleteLabel(ctx2, tc.label.Name)
|
||||
checkAuthErr(t, tc.shouldFailWrite, err)
|
||||
|
||||
err = svc.DeleteLabelByID(ctx2, tc.label.ID)
|
||||
checkAuthErr(t, tc.shouldFailWrite, err)
|
||||
})
|
||||
}
|
||||
|
||||
// filtering is done in the data store for these; NOT enforcing premium license checks here
|
||||
_, err = svc.GetLabelSpec(ctx, "abc")
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.ListHostsInLabel(ctx, 1, fleet.HostListOptions{})
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, _, err = svc.GetLabel(ctx, 1)
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, _, err = svc.ModifyLabel(ctx, otherLabel.ID, fleet.ModifyLabelPayload{})
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
|
||||
// global label listing should work if you can read global labels
|
||||
_, err = svc.GetLabelSpecs(ctx, ptr.Uint(0))
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.ListLabels(ctx, fleet.ListOptions{}, ptr.Uint(0), true)
|
||||
checkAuthErr(t, tt.shouldFailGlobalRead, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue