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{