Add team assignment checks to APIs that do label association (#37246)

Resolves #37104

## Testing

- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Label validation now enforces team-context constraints for policies,
queries, and MDM profiles.
  * Global policies now verify label validity before creation.

* **Bug Fixes**
* Improved label association verification in team-specific
configurations.

* **Tests**
* Added comprehensive test coverage for team label associations,
including label scoping validation and team deletion scenarios.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
Lucas Manuel Rodriguez
2025-12-15 14:11:36 -03:00
committed by GitHub
co-authored by Ian Littman
parent a5b2e911d6
commit 554f268768
20 changed files with 779 additions and 45 deletions
+7 -4
View File
@@ -275,10 +275,13 @@ type Service interface {
// ListLabelsForHost returns a slice of labels for a given host
ListLabelsForHost(ctx context.Context, hostID uint) ([]*Label, error)
// BatchValidateLabels validates that each of the provided label names exists. The returned map
// is keyed by label name. Caller must ensure that appropirate authorization checks are
// performed prior to calling this method.
BatchValidateLabels(ctx context.Context, labelNames []string) (map[string]LabelIdent, error)
// BatchValidateLabels validates that each of the provided label names exists,
// and verifies the provided label names belong to the given teamID.
//
// The returned map is keyed by label name.
// Caller must ensure that appropriate authorization checks are performed prior
// to calling this method.
BatchValidateLabels(ctx context.Context, teamID *uint, labelNames []string) (map[string]LabelIdent, error)
// /////////////////////////////////////////////////////////////////////////////
// QueryService
+3 -3
View File
@@ -162,7 +162,7 @@ type ListHostsInLabelFunc func(ctx context.Context, lid uint, opt fleet.HostList
type ListLabelsForHostFunc func(ctx context.Context, hostID uint) ([]*fleet.Label, error)
type BatchValidateLabelsFunc func(ctx context.Context, labelNames []string) (map[string]fleet.LabelIdent, error)
type BatchValidateLabelsFunc func(ctx context.Context, teamID *uint, labelNames []string) (map[string]fleet.LabelIdent, error)
type ApplyQuerySpecsFunc func(ctx context.Context, specs []*fleet.QuerySpec) error
@@ -2651,11 +2651,11 @@ func (s *Service) ListLabelsForHost(ctx context.Context, hostID uint) ([]*fleet.
return s.ListLabelsForHostFunc(ctx, hostID)
}
func (s *Service) BatchValidateLabels(ctx context.Context, labelNames []string) (map[string]fleet.LabelIdent, error) {
func (s *Service) BatchValidateLabels(ctx context.Context, teamID *uint, labelNames []string) (map[string]fleet.LabelIdent, error) {
s.mu.Lock()
s.BatchValidateLabelsFuncInvoked = true
s.mu.Unlock()
return s.BatchValidateLabelsFunc(ctx, labelNames)
return s.BatchValidateLabelsFunc(ctx, teamID, labelNames)
}
func (s *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpec) error {
+1 -1
View File
@@ -436,7 +436,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, d
cp.Mobileconfig = data
cp.SecretsUpdatedAt = secretsUpdatedAt
labelMap, err := svc.validateProfileLabels(ctx, labels)
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating labels")
}
+5
View File
@@ -75,6 +75,11 @@ func (svc Service) NewGlobalPolicy(ctx context.Context, p fleet.PolicyPayload) (
Message: fmt.Sprintf("policy payload verification: %s", err),
})
}
if err := verifyLabelsToAssociate(ctx, svc.ds, nil, append(p.LabelsIncludeAny, p.LabelsExcludeAny...)); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
policy, err := svc.ds.NewGlobalPolicy(ctx, ptr.Uint(vc.UserID()), p)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "storing policy")
+552 -11
View File
@@ -16917,7 +16917,6 @@ func (s *integrationMDMTestSuite) TestSetupExperience() {
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID1, titleID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &swInstallerPayload1)
_ = installerID1
require.NoError(t, err)
app1 := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"}
@@ -19078,23 +19077,29 @@ func (s *integrationMDMTestSuite) TestTeamLabelsTeamDeletion() {
// Create label on team t1.
l1t1, err := s.ds.NewLabel(t.Context(), &fleet.Label{
Name: "l1t1",
Query: "SELECT t1;",
TeamID: &t1.ID,
Name: "l1t1",
Query: "SELECT t1;",
TeamID: &t1.ID,
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)
// Create two labels on team t2.
// Create a label on team t2.
l2t2, err := s.ds.NewLabel(t.Context(), &fleet.Label{
Name: "l2t2",
Query: "SELECT t2;",
TeamID: &t2.ID,
Name: "l2t2",
Query: "SELECT t2;",
TeamID: &t2.ID,
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)
// Create global label.
globalLabel, err := s.ds.NewLabel(t.Context(), &fleet.Label{
Name: "global",
Query: "SELECT global;",
TeamID: nil,
Name: "global",
Query: "SELECT global;",
TeamID: nil,
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)
@@ -19222,3 +19227,539 @@ func (s *integrationMDMTestSuite) TestTeamLabelsTeamDeletion() {
require.Equal(t, l2t2.ID, hostLabels[0].ID)
require.Equal(t, globalLabel.ID, hostLabels[1].ID)
}
func (s *integrationMDMTestSuite) TestTeamLabelsAssociationsCheck() {
t := s.T()
test.CreateInsertGlobalVPPToken(t, s.ds)
t1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: "t1",
})
require.NoError(t, err)
t2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: "t2",
})
require.NoError(t, err)
// Create label on team t1.
l1t1, err := s.ds.NewLabel(t.Context(), &fleet.Label{
Name: "l1t1",
Query: "SELECT t1;",
TeamID: &t1.ID,
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)
// Create label on team t2.
l2t2, err := s.ds.NewLabel(t.Context(), &fleet.Label{
Name: "l2t2",
Query: "SELECT t2;",
TeamID: &t2.ID,
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)
// Create global label.
globalLabel, err := s.ds.NewLabel(t.Context(), &fleet.Label{
Name: "global",
Query: "SELECT global;",
TeamID: nil,
})
require.NoError(t, err)
t.Run("1. policy labels assignment checks", func(t *testing.T) {
// 1.A.1 Attempt to create global policy that references l1t1 (should fail).
var gpResp globalPolicyResponse
s.DoJSON("POST", "/api/latest/fleet/policies", globalPolicyRequest{
Name: "All teams policy",
Query: "SELECT 1;",
LabelsIncludeAny: []string{l1t1.Name, globalLabel.Name},
}, http.StatusBadRequest, &gpResp)
gpResp = globalPolicyResponse{}
s.DoJSON("POST", "/api/latest/fleet/policies", globalPolicyRequest{
Name: "All teams policy",
Query: "SELECT 1;",
LabelsExcludeAny: []string{globalLabel.Name, l1t1.Name},
}, http.StatusBadRequest, &gpResp)
// 1.A.2 Attempt to create a global policy with global labels (should succeed).
gpResp = globalPolicyResponse{}
s.DoJSON("POST", "/api/latest/fleet/policies", globalPolicyRequest{
Name: "All teams policy",
Query: "SELECT 1;",
LabelsIncludeAny: []string{globalLabel.Name},
}, http.StatusOK, &gpResp)
globalPolicyID := gpResp.Policy.ID
gpResp = globalPolicyResponse{}
s.DoJSON("POST", "/api/latest/fleet/policies", globalPolicyRequest{
Name: "All teams policy 2",
Query: "SELECT 1;",
LabelsExcludeAny: []string{globalLabel.Name},
}, http.StatusOK, &gpResp)
// 1.A.3 Attempt to modify a global policy with team labels (should fail).
mgpr := &modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("newName1"),
LabelsIncludeAny: []string{l1t1.Name},
},
}
patchPol1 := &modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", globalPolicyID), mgpr, http.StatusBadRequest, patchPol1)
mgpr = &modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("newName1"),
LabelsExcludeAny: []string{l1t1.Name},
},
}
patchPol1 = &modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", globalPolicyID), mgpr, http.StatusBadRequest, patchPol1)
// 1.A.4 Attempt to modify a global policy with global labels (should succeed).
mgpr = &modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("newName1"),
LabelsIncludeAny: []string{globalLabel.Name},
},
}
patchPol1 = &modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", globalPolicyID), mgpr, http.StatusOK, patchPol1)
mgpr = &modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
Name: ptr.String("newName2"),
LabelsIncludeAny: []string{},
LabelsExcludeAny: []string{globalLabel.Name},
},
}
patchPol1 = &modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", globalPolicyID), mgpr, http.StatusOK, patchPol1)
// 1.B.1 Attempt to create a team policy that references l2t2 (should fail).
tpResp := teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
Name: "t1 policy",
Query: "SELECT 1;",
LabelsIncludeAny: []string{globalLabel.Name, l2t2.Name},
}, http.StatusBadRequest, &tpResp)
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
Name: "t1 policy exclude",
Query: "SELECT 1;",
LabelsExcludeAny: []string{globalLabel.Name, l2t2.Name},
}, http.StatusBadRequest, &tpResp)
// 1.B.2 Attempt to create a team policy with a global label and same team label (should succeed).
tpResp = teamPolicyResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
Name: "t1 policy",
Query: "SELECT 1;",
LabelsIncludeAny: []string{globalLabel.Name, l1t1.Name},
}, http.StatusOK, &tpResp)
teamPolicyID := tpResp.Policy.ID
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
Name: "t1 policy 2",
Query: "SELECT 1;",
LabelsExcludeAny: []string{globalLabel.Name, l1t1.Name},
}, http.StatusOK, &tpResp)
// 1.B.3 Attempt to edit a team policy to reference l2t2 (should fail; label is outside team).
mtplr := modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t1.ID, teamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{l2t2.Name},
},
}, http.StatusBadRequest, &mtplr)
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t1.ID, teamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{},
LabelsExcludeAny: []string{l2t2.Name},
},
}, http.StatusBadRequest, &mtplr)
// 1.B.3 Attempt to edit a team policy to reference a team label on the same team (should succeed).
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t1.ID, teamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{l1t1.Name},
},
}, http.StatusOK, &mtplr)
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", t1.ID, teamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{},
LabelsExcludeAny: []string{l1t1.Name, globalLabel.Name},
},
}, http.StatusOK, &mtplr)
// 1.C.1 Attempt to create a "No team" policy that references l1t1 (should fail).
tpResp = teamPolicyResponse{}
s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", teamPolicyRequest{
Name: "no team policy",
Query: "SELECT 1;",
LabelsIncludeAny: []string{globalLabel.Name, l2t2.Name},
}, http.StatusBadRequest, &tpResp)
s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", teamPolicyRequest{
Name: "no team policy exclude",
Query: "SELECT 1;",
LabelsExcludeAny: []string{globalLabel.Name, l2t2.Name},
}, http.StatusBadRequest, &tpResp)
// 1.B.2 Attempt to create a "No team" policy with a global label (should succeed).
tpResp = teamPolicyResponse{}
s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", teamPolicyRequest{
Name: "no team policy",
Query: "SELECT 1;",
LabelsIncludeAny: []string{globalLabel.Name},
}, http.StatusOK, &tpResp)
noTeamPolicyID := tpResp.Policy.ID
s.DoJSON("POST", "/api/latest/fleet/teams/0/policies", teamPolicyRequest{
Name: "no team policy 2",
Query: "SELECT 1;",
LabelsExcludeAny: []string{globalLabel.Name},
}, http.StatusOK, &tpResp)
// 1.B.3 Attempt to edit a "No team" policy with a team policy that references l2t2 (should fail).
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/0/policies/%d", noTeamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{l2t2.Name},
},
}, http.StatusBadRequest, &mtplr)
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/0/policies/%d", noTeamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{},
LabelsExcludeAny: []string{l2t2.Name},
},
}, http.StatusBadRequest, &mtplr)
// 1.B.3 Attempt to edit a team policy to reference a global label (should succeed).
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/0/policies/%d", noTeamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{globalLabel.Name},
},
}, http.StatusOK, &mtplr)
mtplr = modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/0/policies/%d", noTeamPolicyID), modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
LabelsIncludeAny: []string{},
LabelsExcludeAny: []string{globalLabel.Name},
},
}, http.StatusOK, &mtplr)
})
t.Run("2. query labels assignment checks", func(t *testing.T) {
// 2.A.1 Attempt to create global query with team labels (should fail).
var createQueryResp createQueryResponse
reqQuery := &fleet.QueryPayload{
Name: ptr.String("All teams query"),
Query: ptr.String("SELECT 1;"),
LabelsIncludeAny: []string{l1t1.Name},
}
s.DoJSON("POST", "/api/latest/fleet/queries", reqQuery, http.StatusBadRequest, &createQueryResp)
// 2.A.2 Attempt to create global query with global label (should succeed).
createQueryResp = createQueryResponse{}
reqQuery = &fleet.QueryPayload{
Name: ptr.String("All teams query"),
Query: ptr.String("SELECT 1;"),
LabelsIncludeAny: []string{globalLabel.Name},
}
s.DoJSON("POST", "/api/latest/fleet/queries", reqQuery, http.StatusOK, &createQueryResp)
globalQueryID := createQueryResp.Query.ID
// 2.A.3 Attempt to edit global query with team label (should fail).
modifyQueryResp := modifyQueryResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", globalQueryID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
LabelsIncludeAny: []string{l1t1.Name},
},
}, http.StatusBadRequest, &modifyQueryResp)
// 2.A.4 Attempt to edit global query with global label (should succeed).
modifyQueryResp = modifyQueryResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", globalQueryID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
LabelsIncludeAny: []string{globalLabel.Name},
},
}, http.StatusOK, &modifyQueryResp)
// 2.B.1 Attempt to create a team query with a label of another team (should fail).
createQueryResp = createQueryResponse{}
reqQuery = &fleet.QueryPayload{
Name: ptr.String("Team one query"),
Query: ptr.String("SELECT 1;"),
LabelsIncludeAny: []string{l2t2.Name},
TeamID: &t1.ID,
}
s.DoJSON("POST", "/api/latest/fleet/queries", reqQuery, http.StatusBadRequest, &createQueryResp)
// 2.B.2 Attempt to create a team query with a label of the same team (should succeed).
createQueryResp = createQueryResponse{}
reqQuery = &fleet.QueryPayload{
Name: ptr.String("Team one query"),
Query: ptr.String("SELECT 1;"),
LabelsIncludeAny: []string{l1t1.Name, globalLabel.Name},
TeamID: &t1.ID,
}
s.DoJSON("POST", "/api/latest/fleet/queries", reqQuery, http.StatusOK, &createQueryResp)
team1LabelID := createQueryResp.Query.ID
// 2.A.3 Attempt to edit a team query with a label of another team (should fail).
modifyQueryResp = modifyQueryResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", team1LabelID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
LabelsIncludeAny: []string{l2t2.Name, globalLabel.Name},
},
}, http.StatusBadRequest, &modifyQueryResp)
// 2.A.4 Attempt to edit team query with a label of the same team (should succeed).
modifyQueryResp = modifyQueryResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", team1LabelID), modifyQueryRequest{
QueryPayload: fleet.QueryPayload{
LabelsIncludeAny: []string{l1t1.Name},
},
}, http.StatusOK, &modifyQueryResp)
})
t.Run("3. configuration profiles assignment check", func(t *testing.T) {
// NOTE: Not testing the API endpoint POST /api/latest/fleet/mdm/profiles used by the UI
// because of time constraints (we haven't yet implemented test utilities for multipart uploads).
// Attempt to create a team profile with labels from another team (should fail).
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{
ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
{
DisplayName: "N1", Profile: mobileconfigForTestWithContent("N1", "I1", "com.test.profile.content", "test inner type", "test inner name"),
LabelsIncludeAll: []string{l2t2.Name},
},
},
}, http.StatusBadRequest, "team_id", fmt.Sprint(t1.ID))
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{
ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
{
DisplayName: "N2", Profile: syncMLForTest("./Foo/Bar"),
LabelsIncludeAll: []string{l2t2.Name},
},
},
}, http.StatusBadRequest, "team_id", fmt.Sprint(t1.ID))
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{
ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
{
DisplayName: "N3", Profile: declarationForTest("D1"),
LabelsIncludeAll: []string{l2t2.Name},
},
},
}, http.StatusBadRequest, "team_id", fmt.Sprint(t1.ID))
// Attempt to create a profile with a label on the same team (should succeed).
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{
ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
{
DisplayName: "N3", Profile: declarationForTest("D1"),
LabelsIncludeAll: []string{l1t1.Name},
},
},
}, http.StatusNoContent, "team_id", fmt.Sprint(t1.ID))
// Attempt to create a profile in "No team" with a label that belongs to a team (should fail).
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{
ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
{
DisplayName: "N1", Profile: mobileconfigForTestWithContent("N1", "I1", "com.test.profile.content", "test inner type", "test inner name"),
LabelsIncludeAll: []string{l1t1.Name},
},
},
}, http.StatusBadRequest)
// Attempt to create a profile in "No team" with a global label (should succeed).
s.Do("POST", "/api/latest/fleet/configuration_profiles/batch", batchModifyMDMConfigProfilesRequest{
ConfigurationProfiles: []fleet.BatchModifyMDMConfigProfilePayload{
{
DisplayName: "N1", Profile: mobileconfigForTestWithContent("N1", "I1", "com.test.profile.content", "test inner type", "test inner name"),
LabelsIncludeAll: []string{globalLabel.Name},
},
},
}, http.StatusNoContent)
})
t.Run("4. software installers assignment check", func(t *testing.T) {
// Attempt to create a software installer with a label of another team (should fail).
payloadRubyTm1 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "ruby.deb",
SelfService: false,
TeamID: &t1.ID,
LabelsIncludeAny: []string{l2t2.Name},
Platform: "linux",
}
s.uploadSoftwareInstaller(t, payloadRubyTm1, http.StatusBadRequest, "")
// Attempt to create a software installer with a label on the same team (should succeed).
payloadRubyTm1 = &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "ruby.deb",
SelfService: false,
TeamID: &t1.ID,
LabelsIncludeAny: []string{l1t1.Name},
Platform: "linux",
}
s.uploadSoftwareInstaller(t, payloadRubyTm1, http.StatusOK, "")
swTitleID := getSoftwareTitleID(t, s.ds, "ruby", "deb_packages")
// Attempt to edit a software installer with a label from another team (should fail).
updatePayload := &fleet.UpdateSoftwareInstallerPayload{
TitleID: swTitleID,
TeamID: &t1.ID,
LabelsIncludeAny: []string{l2t2.Name},
}
s.updateSoftwareInstaller(t, updatePayload, http.StatusBadRequest, "")
// Attempt to edit a software installer with a label on the same team (should succeed).
updatePayload = &fleet.UpdateSoftwareInstallerPayload{
TitleID: swTitleID,
TeamID: &t1.ID,
LabelsIncludeAny: []string{l1t1.Name, globalLabel.Name},
}
s.updateSoftwareInstaller(t, updatePayload, http.StatusOK, "")
// Attempt to create a software installer in "No team" with a team label (should fail).
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "dummy_installer.pkg",
Title: "DummyApp",
TeamID: nil,
LabelsExcludeAny: []string{l1t1.Name},
}
s.uploadSoftwareInstaller(t, payload, http.StatusBadRequest, "")
// Attempt to create a software installer in "No team" with a global label (should succeed).
payload = &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "dummy_installer.pkg",
Title: "DummyApp",
TeamID: nil,
LabelsExcludeAny: []string{globalLabel.Name},
}
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
swTitleID = getSoftwareTitleID(t, s.ds, payload.Title, "apps")
// Attempt to edit a software installer in "No team" with a team label (should fail).
updatePayload = &fleet.UpdateSoftwareInstallerPayload{
TitleID: swTitleID,
TeamID: nil,
LabelsIncludeAny: []string{l1t1.Name, globalLabel.Name},
}
s.updateSoftwareInstaller(t, updatePayload, http.StatusBadRequest, "")
// Attempt to edit a software installer in "No team" with a global label (should succeed).
updatePayload = &fleet.UpdateSoftwareInstallerPayload{
TitleID: swTitleID,
TeamID: nil,
LabelsIncludeAny: []string{globalLabel.Name},
}
s.updateSoftwareInstaller(t, updatePayload, http.StatusOK, "")
})
t.Run("5. vpp apps assignment checks", func(t *testing.T) {
// Set up VPP token
orgName := "Fleet Device Management Inc."
token := "mycooltoken"
expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
expDate := expTime.Format(fleet.VPPTimeFormat)
tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
var validToken uploadVPPTokenResponse
s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)
// Get the token
var resp getVPPTokensResponse
s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
require.NoError(t, resp.Err)
// Associate team to the VPP token.
var resPatchVPP patchVPPTokensTeamsResponse
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{t1.ID}}, http.StatusOK, &resPatchVPP)
// Attempt to add an app store app on a team with a label from another team (should fail).
var addAppResp addAppStoreAppResponse
addAppReq := &addAppStoreAppRequest{
TeamID: &t1.ID,
AppStoreID: "1",
SelfService: true,
LabelsIncludeAny: []string{l2t2.Name},
}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusBadRequest, &addAppResp)
// Attempt to add an app store app on a team with a label on the same team (should succeed).
addAppReq = &addAppStoreAppRequest{
TeamID: &t1.ID,
AppStoreID: "1",
SelfService: true,
LabelsIncludeAny: []string{l1t1.Name},
}
addAppResp = addAppStoreAppResponse{}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusOK, &addAppResp)
// Associate all teams token.
resPatchVPP = patchVPPTokensTeamsResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
// Attempt to add an app store app to "No team" with a team label (should fail).
addAppResp = addAppStoreAppResponse{}
addAppReq = &addAppStoreAppRequest{
TeamID: nil,
AppStoreID: "1",
SelfService: true,
LabelsIncludeAny: []string{l2t2.Name},
}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusBadRequest, &addAppResp)
// Attempt to add an app store app to "No team" with a global label (should succeed).
addAppResp = addAppStoreAppResponse{}
addAppReq = &addAppStoreAppRequest{
TeamID: nil,
AppStoreID: "1",
SelfService: true,
LabelsIncludeAny: []string{globalLabel.Name},
}
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusOK, &addAppResp)
})
t.Run("6. in-house apps assignment checks", func(t *testing.T) {
// Attempt to create in-house app in team t1 that references l2t2 (should fail).
payload := &fleet.UploadSoftwareInstallerPayload{
TeamID: &t1.ID,
Filename: "ipa_test2.ipa",
Version: "1.0.0",
StorageID: uuid.New().String(),
SelfService: true,
LabelsIncludeAny: []string{l2t2.Name},
}
s.uploadSoftwareInstaller(t, payload, http.StatusBadRequest, "")
// Attempt to create in-house app in team t1 that references l1t1 (should succeed).
payload = &fleet.UploadSoftwareInstallerPayload{
TeamID: &t1.ID,
Filename: "ipa_test2.ipa",
Version: "1.0.0",
StorageID: uuid.New().String(),
SelfService: true,
LabelsIncludeAny: []string{l1t1.Name},
}
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
// Not testing edit as this was tested in (4) above.
// This is used to populate the in house apps tables.
})
// Make sure we can delete the teams given all the entities created.
err = s.ds.DeleteTeam(t.Context(), t1.ID)
require.NoError(t, err)
err = s.ds.DeleteTeam(t.Context(), t2.ID)
require.NoError(t, err)
}
+5 -1
View File
@@ -680,7 +680,7 @@ func (svc *Service) GetLabelSpec(ctx context.Context, name string) (*fleet.Label
return svc.ds.GetLabelSpec(ctx, name)
}
func (svc *Service) BatchValidateLabels(ctx context.Context, labelNames []string) (map[string]fleet.LabelIdent, error) {
func (svc *Service) BatchValidateLabels(ctx context.Context, teamID *uint, labelNames []string) (map[string]fleet.LabelIdent, error) {
if authctx, ok := authz_ctx.FromContext(ctx); !ok {
return nil, fleet.NewAuthRequiredError("batch validate labels: missing authorization context")
} else if !authctx.Checked() {
@@ -705,6 +705,10 @@ func (svc *Service) BatchValidateLabels(ctx context.Context, labelNames []string
}
}
if err := verifyLabelsToAssociate(ctx, svc.ds, teamID, labelNames); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
byName := make(map[string]fleet.LabelIdent, len(labels))
for labelName, labelID := range labels {
byName[labelName] = fleet.LabelIdent{
+18 -3
View File
@@ -375,7 +375,7 @@ func TestBatchValidateLabels(t *testing.T) {
svc, ctx := newTestService(t, ds, nil, nil)
t.Run("no auth context", func(t *testing.T) {
_, err := svc.BatchValidateLabels(context.Background(), nil)
_, err := svc.BatchValidateLabels(context.Background(), nil, nil)
require.ErrorContains(t, err, "Authentication required")
})
@@ -383,7 +383,7 @@ func TestBatchValidateLabels(t *testing.T) {
ctx = authz_ctx.NewContext(ctx, &authCtx)
t.Run("no auth checked", func(t *testing.T) {
_, err := svc.BatchValidateLabels(ctx, nil)
_, err := svc.BatchValidateLabels(ctx, nil, nil)
require.ErrorContains(t, err, "Authentication required")
})
@@ -413,6 +413,21 @@ func TestBatchValidateLabels(t *testing.T) {
}
return res, nil
}
ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
res := make(map[string]*fleet.Label)
if names == nil {
return res, nil
}
for _, name := range names {
if id, ok := mockLabels[name]; ok {
res[name] = &fleet.Label{
ID: id,
Name: name,
}
}
}
return res, nil
}
testCases := []struct {
name string
@@ -464,7 +479,7 @@ func TestBatchValidateLabels(t *testing.T) {
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, err := svc.BatchValidateLabels(ctx, tt.labelNames)
got, err := svc.BatchValidateLabels(ctx, nil, tt.labelNames)
if tt.expectError != "" {
require.Contains(t, err.Error(), tt.expectError)
} else {
+72
View File
@@ -0,0 +1,72 @@
package service
import (
"context"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
)
func loadLabelsFromNames(ctx context.Context, ds fleet.Datastore, labelNames []string) (map[string]*fleet.Label, error) {
labelsMap, err := ds.LabelsByName(ctx, labelNames)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get labels by name")
}
// Make sure that all labels were found
for _, labelName := range labelNames {
if _, ok := labelsMap[labelName]; !ok {
return nil, ctxerr.Wrap(ctx, badRequestf("label %q not found", labelName))
}
}
return labelsMap, nil
}
func verifyLabelsToAssociate(ctx context.Context, ds fleet.Datastore, entityTeamID *uint, labelNames []string) error {
if len(labelNames) == 0 {
return nil
}
// Remove duplicate names.
seen := make(map[string]struct{})
uniqueLabelNames := make([]string, 0, len(labelNames))
for _, s := range labelNames {
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
uniqueLabelNames = append(uniqueLabelNames, s)
}
// Load data of all labels.
labels, err := loadLabelsFromNames(ctx, ds, uniqueLabelNames)
if err != nil {
return ctxerr.Wrap(ctx, err, "labels by name")
}
// Perform team ID checks for "No team" or global entities.
if entityTeamID == nil || *entityTeamID == 0 {
// entityTeamID == nil: global entity (like "All teams" policies and "All team" queries)
// entityTeamID == 0: "no team" entity.
// For both cases, labels must be global because currently we don't support labels in "No team".
for _, label := range labels {
if label.TeamID != nil {
return ctxerr.Wrap(ctx, badRequestf("label %q is a team label", label.Name))
}
}
return nil
}
// Perform team ID checks for team entities.
for _, label := range labels {
// Team entities can reference global labels.
if label.TeamID == nil {
continue
}
// Team entities cannot reference labels that belong another team.
if *label.TeamID != *entityTeamID {
return ctxerr.Wrap(ctx, badRequestf("label %q belongs to a different team", label.Name))
}
}
return nil
}
+20 -7
View File
@@ -1769,7 +1769,7 @@ func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint,
return nil, ctxerr.Wrap(ctx, err, "validate profile")
}
labelMap, err := svc.validateProfileLabels(ctx, labels)
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating labels")
}
@@ -1814,7 +1814,7 @@ func (svc *Service) NewMDMAndroidConfigProfile(ctx context.Context, teamID uint,
return newCP, nil
}
func (svc *Service) batchValidateProfileLabels(ctx context.Context, labelNames []string) (map[string]fleet.ConfigurationProfileLabel, error) {
func (svc *Service) batchValidateProfileLabels(ctx context.Context, teamID *uint, labelNames []string) (map[string]fleet.ConfigurationProfileLabel, error) {
if len(labelNames) == 0 {
return nil, nil
}
@@ -1838,6 +1838,13 @@ func (svc *Service) batchValidateProfileLabels(ctx context.Context, labelNames [
}
}
// NOTE(lucas): To not break API error string returned above
// AND for code reusability we are a-ok with loading labels again in verifyLabelsToAssociate.
// This can definitely be optimized if need be.
if err := verifyLabelsToAssociate(ctx, svc.ds, teamID, labelNames); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
profLabels := make(map[string]fleet.ConfigurationProfileLabel)
for labelName, labelID := range labels {
profLabels[labelName] = fleet.ConfigurationProfileLabel{
@@ -1848,8 +1855,8 @@ func (svc *Service) batchValidateProfileLabels(ctx context.Context, labelNames [
return profLabels, nil
}
func (svc *Service) validateProfileLabels(ctx context.Context, labelNames []string) ([]fleet.ConfigurationProfileLabel, error) {
labelMap, err := svc.batchValidateProfileLabels(ctx, labelNames)
func (svc *Service) validateProfileLabels(ctx context.Context, teamID *uint, labelNames []string) ([]fleet.ConfigurationProfileLabel, error) {
labelMap, err := svc.batchValidateProfileLabels(ctx, teamID, labelNames)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating profile labels")
}
@@ -1959,8 +1966,14 @@ func batchSetMDMProfilesEndpoint(ctx context.Context, request interface{}, svc f
}
func (svc *Service) BatchSetMDMProfiles(
ctx context.Context, tmID *uint, tmName *string, profiles []fleet.MDMProfileBatchPayload, dryRun, skipBulkPending bool,
assumeEnabled *bool, noCache bool,
ctx context.Context,
tmID *uint,
tmName *string,
profiles []fleet.MDMProfileBatchPayload,
dryRun bool,
skipBulkPending bool,
assumeEnabled *bool,
noCache bool,
) error {
var err error
if tmID, tmName, err = svc.authorizeBatchProfiles(ctx, tmID, tmName); err != nil {
@@ -2000,7 +2013,7 @@ func (svc *Service) BatchSetMDMProfiles(
}
var labelMap map[string]fleet.ConfigurationProfileLabel
if !dryRun {
labelMap, err = svc.batchValidateProfileLabels(ctx, labels)
labelMap, err = svc.batchValidateProfileLabels(ctx, tmID, labels)
if err != nil {
return ctxerr.Wrap(ctx, err, "validating labels")
}
+13
View File
@@ -2360,6 +2360,19 @@ func TestBatchSetMDMProfilesLabels(t *testing.T) {
}
return m, nil
}
ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
m := map[string]*fleet.Label{}
for _, name := range names {
if name != "baddy" {
labelID++
m[name] = &fleet.Label{
ID: labelID,
Name: name,
}
}
}
return m, nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
+9
View File
@@ -281,6 +281,10 @@ func (svc *Service) NewQuery(ctx context.Context, p fleet.QueryPayload) (*fleet.
})
}
if err := verifyLabelsToAssociate(ctx, svc.ds, p.TeamID, p.LabelsIncludeAny); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
query := &fleet.Query{
Saved: true,
@@ -417,6 +421,11 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
})
}
// We use query.TeamID because we do not allow changing the team
if err := verifyLabelsToAssociate(ctx, svc.ds, query.TeamID, p.LabelsIncludeAny); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
shouldDiscardQueryResults, shouldDeleteStats := false, false
if p.Name != nil {
+18 -3
View File
@@ -210,7 +210,7 @@ func TestValidateSoftwareLabels(t *testing.T) {
t.Run("validate no update", func(t *testing.T) {
t.Run("no auth context", func(t *testing.T) {
_, err := eeservice.ValidateSoftwareLabels(context.Background(), svc, nil, nil)
_, err := eeservice.ValidateSoftwareLabels(context.Background(), svc, nil, nil, nil)
require.ErrorContains(t, err, "Authentication required")
})
@@ -218,7 +218,7 @@ func TestValidateSoftwareLabels(t *testing.T) {
ctx = authz_ctx.NewContext(ctx, &authCtx)
t.Run("no auth checked", func(t *testing.T) {
_, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, nil)
_, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, nil, nil)
require.ErrorContains(t, err, "Authentication required")
})
@@ -244,6 +244,21 @@ func TestValidateSoftwareLabels(t *testing.T) {
}
return res, nil
}
ds.LabelsByNameFunc = func(ctx context.Context, names []string) (map[string]*fleet.Label, error) {
res := make(map[string]*fleet.Label)
if names == nil {
return res, nil
}
for _, name := range names {
if id, ok := mockLabels[name]; ok {
res[name] = &fleet.Label{
ID: id,
Name: name,
}
}
}
return res, nil
}
testCases := []struct {
name string
@@ -328,7 +343,7 @@ func TestValidateSoftwareLabels(t *testing.T) {
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got, err := eeservice.ValidateSoftwareLabels(ctx, svc, tt.payloadIncludeAny, tt.payloadExcludeAny)
got, err := eeservice.ValidateSoftwareLabels(ctx, svc, nil, tt.payloadIncludeAny, tt.payloadExcludeAny)
if tt.expectError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectError)
+9
View File
@@ -90,6 +90,11 @@ func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, tp fleet.NewT
Message: fmt.Sprintf("policy payload verification: %s", err),
})
}
if err := verifyLabelsToAssociate(ctx, svc.ds, &teamID, append(tp.LabelsIncludeAny, tp.LabelsExcludeAny...)); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
policy, err := svc.ds.NewTeamPolicy(ctx, teamID, ptr.Uint(vc.UserID()), p)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating policy")
@@ -565,6 +570,10 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
})
}
if err := verifyLabelsToAssociate(ctx, svc.ds, teamID, append(p.LabelsIncludeAny, p.LabelsExcludeAny...)); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify labels to associate")
}
var removeAllMemberships bool
var removeStats bool
if p.Name != nil {
+10 -1
View File
@@ -148,7 +148,16 @@ func (ts *withServer) commonTearDownTest(t *testing.T) {
}
_, err = q.ExecContext(ctx, "DELETE FROM in_house_apps;")
return err
if err != nil {
return err
}
_, err = q.ExecContext(ctx, "DELETE FROM vpp_apps;")
if err != nil {
return err
}
return nil
})
lbls, err := ts.ds.ListLabels(ctx, fleet.TeamFilter{}, fleet.ListOptions{})
+1 -1
View File
@@ -61,7 +61,7 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
return nil, ctxerr.Wrap(ctx, err, "validate profile")
}
labelMap, err := svc.validateProfileLabels(ctx, labels)
labelMap, err := svc.validateProfileLabels(ctx, &teamID, labels)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating labels")
}