diff --git a/changes/21264-fix-reserved-team-names b/changes/21264-fix-reserved-team-names new file mode 100644 index 0000000000..6363b81869 --- /dev/null +++ b/changes/21264-fix-reserved-team-names @@ -0,0 +1,2 @@ +- Prevents teams with the name "All teams" or "No team" from being created (these are reserved team + names in Fleet). \ No newline at end of file diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index ba4124f4e0..63528fea2d 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -395,6 +395,27 @@ software: require.Error(t, err) assert.Contains(t, err.Error(), "'name' is required") + // reserved team name; should error in both dry run and real + t.Setenv("TEST_TEAM_NAME", "no TEam") + _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"}) + require.Error(t, err) + assert.Contains(t, err.Error(), `"No team" is a reserved team name`) + + t.Setenv("TEST_TEAM_NAME", "no TEam") + _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()}) + require.Error(t, err) + assert.Contains(t, err.Error(), `"No team" is a reserved team name`) + + t.Setenv("TEST_TEAM_NAME", "All teams") + _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"}) + require.Error(t, err) + assert.Contains(t, err.Error(), `"All teams" is a reserved team name`) + + t.Setenv("TEST_TEAM_NAME", "All TEAMS") + _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()}) + require.Error(t, err) + assert.Contains(t, err.Error(), `"All teams" is a reserved team name`) + // Dry run t.Setenv("TEST_TEAM_NAME", teamName) _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"}) diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index bb8e01342b..448abc3674 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server" @@ -73,6 +74,13 @@ func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Te if *p.Name == "" { return nil, fleet.NewInvalidArgumentError("name", "may not be empty") } + l := strings.ToLower(*p.Name) + if l == strings.ToLower(fleet.ReservedNameAllTeams) { + return nil, fleet.NewInvalidArgumentError("name", `"All teams" is a reserved team name`) + } + if l == strings.ToLower(fleet.ReservedNameNoTeam) { + return nil, fleet.NewInvalidArgumentError("name", `"No team" is a reserved team name`) + } team.Name = *p.Name if p.Description != nil { @@ -129,6 +137,13 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T if *payload.Name == "" { return nil, fleet.NewInvalidArgumentError("name", "may not be empty") } + l := strings.ToLower(*payload.Name) + if l == strings.ToLower(fleet.ReservedNameAllTeams) { + return nil, fleet.NewInvalidArgumentError("name", `"All teams" is a reserved team name`) + } + if l == strings.ToLower(fleet.ReservedNameNoTeam) { + return nil, fleet.NewInvalidArgumentError("name", `"No team" is a reserved team name`) + } team.Name = *payload.Name } if payload.Description != nil { @@ -860,6 +875,14 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, } } + l := strings.ToLower(spec.Name) + if l == strings.ToLower(fleet.ReservedNameAllTeams) { + return nil, fleet.NewInvalidArgumentError("name", `"All teams" is a reserved team name`) + } + if l == strings.ToLower(fleet.ReservedNameNoTeam) { + return nil, fleet.NewInvalidArgumentError("name", `"No team" is a reserved team name`) + } + var team *fleet.Team // If filename is provided, try to find the team by filename first. // This is needed in case user is trying to modify the team name. @@ -883,6 +906,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, } } } + var create bool if team == nil { team, err = svc.ds.TeamByName(ctx, spec.Name) diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 90a3d609d9..dc7d191ec2 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -337,11 +337,18 @@ const TeamDetailsWrapper = ({ setBackendValidators({ name: "A team with this name already exists", }); + } else if (errorObject.base.includes("all teams")) { + setBackendValidators({ + name: `"All teams" is a reserved team name. Please try another name.`, + }); + } else if (errorObject.base.includes("no team")) { + setBackendValidators({ + name: `"No team" is a reserved team name. Please try another name.`, + }); } else { renderFlash("error", "Could not create team. Please try again."); } } finally { - toggleRenameTeamModal(); setIsUpdatingTeams(false); } }, diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx index 92bbec0280..7243f01eed 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx @@ -116,6 +116,14 @@ const TeamManagementPage = (): JSX.Element => { setBackendValidators({ name: "A team with this name already exists", }); + } else if (createError.data.errors[0].reason.includes("all teams")) { + setBackendValidators({ + name: `"All teams" is a reserved team name. Please try another name.`, + }); + } else if (createError.data.errors[0].reason.includes("no team")) { + setBackendValidators({ + name: `"No team" is a reserved team name. Please try another name.`, + }); } else { renderFlash("error", "Could not create team. Please try again."); toggleCreateTeamModal(); @@ -185,6 +193,16 @@ const TeamManagementPage = (): JSX.Element => { setBackendValidators({ name: "A team with this name already exists", }); + } else if ( + updateError.data.errors[0].reason.includes("all teams") + ) { + setBackendValidators({ + name: `"All teams" is a reserved team name.`, + }); + } else if (updateError.data.errors[0].reason.includes("no team")) { + setBackendValidators({ + name: `"No team" is a reserved team name. Please try another name.`, + }); } else { renderFlash( "error", diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index b4f4148bb3..838a06936f 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1120,6 +1120,20 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { tmResp.Team = nil s.DoJSON("POST", "/api/latest/fleet/teams", team2, http.StatusConflict, &tmResp) + // create a team with reserved team names; should be case-insensitive + teamReserved := &fleet.Team{ + Name: "no TeAm", + Description: "description", + Secrets: []*fleet.EnrollSecret{{Secret: "foobar"}}, + } + + r := s.Do("POST", "/api/latest/fleet/teams", teamReserved, http.StatusUnprocessableEntity) + require.Contains(t, extractServerErrorText(r.Body), `"No team" is a reserved team name`) + + teamReserved.Name = "AlL TeaMS" + r = s.Do("POST", "/api/latest/fleet/teams", teamReserved, http.StatusUnprocessableEntity) + require.Contains(t, extractServerErrorText(r.Body), `"All teams" is a reserved team name`) + // create a team with too many secrets team3 := &fleet.Team{ Name: name + "lots_of_secrets", @@ -1219,6 +1233,13 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { modifyExpiry.HostExpirySettings.HostExpiryWindow = 0 s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), modifyExpiry, http.StatusUnprocessableEntity, &tmResp) + // try to rename to reserved names + r = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), fleet.TeamPayload{Name: ptr.String("no TEAM")}, http.StatusUnprocessableEntity) + require.Contains(t, extractServerErrorText(r.Body), `"No team" is a reserved team name`) + + r = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), fleet.TeamPayload{Name: ptr.String("ALL teAMs")}, http.StatusUnprocessableEntity) + require.Contains(t, extractServerErrorText(r.Body), `"All teams" is a reserved team name`) + // Modify team's calendar config modifyCalendar := fleet.TeamPayload{ Integrations: &fleet.TeamIntegrations{ diff --git a/server/service/teams.go b/server/service/teams.go index 1d8a30032d..d2bcf6a994 100644 --- a/server/service/teams.go +++ b/server/service/teams.go @@ -5,11 +5,12 @@ import ( "crypto/x509" "encoding/json" "fmt" - "golang.org/x/text/unicode/norm" "io" "net/http" "net/url" + "golang.org/x/text/unicode/norm" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" )