From fc6c7b315e6b494f6ff27fd7d840cc51976302c4 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Wed, 25 Feb 2026 12:28:04 -0600 Subject: [PATCH] Use "unassigned" in addition to / in place of "no teams" in back end checks and messages (#40351) **Related issue:** For #40348 # Details This PR replaces the use of "No team" with "Unassigned" and "All teams" with "All fleets" in appropriate checks and error messages. Specifically it restricts using "All fleets" or "Unassigned" as team names # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. n/a ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually * tested attempting to add "no team", "all teams", "unassigned" and "all fleets" as teams and saw appropriate error message --- cmd/fleetctl/fleetctl/gitops_test.go | 4 +-- ee/server/service/teams.go | 25 +++++-------------- .../TeamManagementPage/TeamManagementPage.tsx | 16 ++++++------ server/fleet/teams.go | 16 +++++++++--- server/service/integration_enterprise_test.go | 8 +++--- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index 901c3c05c02..99791069c2e 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -824,12 +824,12 @@ software: 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`) + assert.Contains(t, err.Error(), `is a reserved fleet 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`) + assert.Contains(t, err.Error(), `is a reserved fleet name`) // Dry run t.Setenv("TEST_TEAM_NAME", teamName) diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index be12d38bdaf..fc56354a1a5 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -9,7 +9,6 @@ import ( "maps" "net/http" "net/url" - "strings" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server" @@ -84,12 +83,8 @@ 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`) + if fleet.IsReservedTeamName(*p.Name) { + return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("%q is a reserved fleet name", *p.Name)) } team.Name = *p.Name @@ -152,12 +147,8 @@ 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`) + if fleet.IsReservedTeamName(*payload.Name) { + return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("%q is a reserved fleet name", *payload.Name)) } team.Name = *payload.Name } @@ -1051,12 +1042,8 @@ 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`) + if fleet.IsReservedTeamName(spec.Name) { + return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("%q is a reserved fleet name", spec.Name)) } var team *fleet.Team diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx index 7db23489eef..104e7fb36b1 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx @@ -119,17 +119,19 @@ const TeamManagementPage = (): JSX.Element => { refetchTeams(); }) .catch((createError: { data: IApiError }) => { - if (createError.data.errors[0].reason.includes("Duplicate")) { + const errMsg = createError.data.errors[0].reason.toLowerCase(); + if (errMsg.includes("duplicate")) { setBackendValidators({ name: "A fleet with this name already exists", }); - } else if (createError.data.errors[0].reason.includes("All teams")) { + } else if ( + errMsg.includes("all teams") || + errMsg.includes("all fleets") || + errMsg.includes("no team") || + errMsg.includes("unassigned") + ) { setBackendValidators({ - name: `"All fleets" is a reserved fleet name. Please try another name.`, - }); - } else if (createError.data.errors[0].reason.includes("No team")) { - setBackendValidators({ - name: `"Unassigned" is a reserved fleet name. Please try another name.`, + name: `"${formData.name}" is a reserved fleet name. Please try another name.`, }); } else { renderFlash("error", "Could not create fleet. Please try again."); diff --git a/server/fleet/teams.go b/server/fleet/teams.go index dd10cce715e..61103fe4c49 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "strings" "time" "github.com/fleetdm/fleet/v4/pkg/optjson" @@ -27,10 +28,19 @@ const ( ReservedNameNoTeam = "No team" ) -// IsReservedTeamName checks if the name provided is a reserved team name +// Display names used in user-facing error messages. +const ( + DisplayNameNoTeam = "Unassigned" + DisplayNameAllTeams = "All fleets" +) + +// IsReservedTeamName checks if the name provided is a reserved fleet name (case-insensitive). +// Both old names ("No team", "All teams") and new display names ("Unassigned", "All fleets") +// are reserved to prevent creating teams with any of these names. func IsReservedTeamName(name string) bool { - normalizedName := norm.NFC.String(name) - return normalizedName == ReservedNameAllTeams || normalizedName == ReservedNameNoTeam + normalizedName := strings.ToLower(norm.NFC.String(name)) + return normalizedName == "no team" || normalizedName == "all teams" || + normalizedName == "unassigned" || normalizedName == "all fleets" } type TeamPayload struct { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 290cd5eb944..4c4578b384c 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1671,11 +1671,11 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { } r := s.Do("POST", "/api/latest/fleet/teams", teamReserved, http.StatusUnprocessableEntity) - require.Contains(t, extractServerErrorText(r.Body), `"No team" is a reserved team name`) + require.Contains(t, extractServerErrorText(r.Body), `is a reserved fleet 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`) + require.Contains(t, extractServerErrorText(r.Body), `is a reserved fleet name`) // create a team with too many secrets team3 := &fleet.Team{ @@ -1778,10 +1778,10 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { // 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`) + require.Contains(t, extractServerErrorText(r.Body), `is a reserved fleet 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`) + require.Contains(t, extractServerErrorText(r.Body), `is a reserved fleet name`) // Modify team's calendar config modifyCalendar := fleet.TeamPayload{