Add fleet-user creation check (#47566)

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

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

## Summary by CodeRabbit

* **Tests**
* Added test coverage to verify team administrator access controls and
boundary enforcement. The test ensures that team admins cannot create
users in other teams, validating this restriction across both standard
and API-only user creation endpoints. The test also confirms that users
can be successfully created within their authorized team scope.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lucas Manuel Rodriguez
2026-06-15 07:59:32 -03:00
committed by GitHub
parent 4fdd4bd3b4
commit 6d997bdfda
@@ -5124,6 +5124,83 @@ func (s *integrationEnterpriseTestSuite) TestInvitedUserMFA() {
require.True(t, updateInviteResp.Invite.MFAEnabled)
}
// A fleet-admin must not be able to create a user with roles in teams they don't administer,
// even when one team in the payload is their own.
//
// Both the admin create path (POST /users) and the API-only create
// path (POST /users/api_only) funnel through svc.CreateUser, so both are
// exercised here.
func (s *integrationEnterpriseTestSuite) TestTeamAdminCannotCreateUserInOtherTeams() {
t := s.T()
ctx := context.Background()
// team1 is administered by our caller; team2 is not.
team1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "_team1"})
require.NoError(t, err)
team2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "_team2"})
require.NoError(t, err)
defer func() {
s.token = s.getTestAdminToken()
require.NoError(t, s.ds.DeleteTeam(ctx, team1.ID))
require.NoError(t, s.ds.DeleteTeam(ctx, team2.ID))
}()
// Create a user who is an admin of team1 only.
teamAdminEmail := t.Name() + "_team1_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))
_, err = s.ds.NewUser(ctx, teamAdmin)
require.NoError(t, err)
// Act as the team1 admin for the rest of the test.
s.token = s.getTestToken(teamAdmin.Email, test.GoodPassword)
var resp createUserResponse
// Admin create path: a payload spanning team1 (allowed) and team2 (not
// allowed) must be rejected wholesale, not partially honored.
s.DoJSON("POST", "/api/latest/fleet/users/admin", fleet.UserPayload{
Name: new(t.Name() + "_crossteam"),
Email: new(t.Name() + "_crossteam@example.com"),
Password: new(test.GoodPassword),
Teams: &[]fleet.UserTeam{
{Team: *team1, Role: fleet.RoleAdmin},
{Team: *team2, Role: fleet.RoleAdmin},
},
}, http.StatusForbidden, &resp)
// The forbidden request must not have created the user.
_, err = s.ds.UserByEmail(ctx, t.Name()+"_crossteam@example.com")
require.True(t, fleet.IsNotFound(err))
// API-only create path inherits the same authorization, so it must reject
// the cross-team payload too.
s.DoJSON("POST", "/api/latest/fleet/users/api_only", createAPIOnlyUserRequest{
Name: new(t.Name() + "_crossteam_api"),
Fleets: &[]fleetsPayload{
{ID: team1.ID, Role: fleet.RoleAdmin},
{ID: team2.ID, Role: fleet.RoleAdmin},
},
}, http.StatusForbidden, &resp)
// Sanity check: the team1 admin can still create a user scoped entirely to
// their own team, so the fix doesn't over-restrict the legitimate path.
resp = createUserResponse{}
s.DoJSON("POST", "/api/latest/fleet/users/admin", fleet.UserPayload{
Name: new(t.Name() + "_team1only"),
Email: new(t.Name() + "_team1only@example.com"),
Password: new(test.GoodPassword),
Teams: &[]fleet.UserTeam{
{Team: *team1, Role: fleet.RoleAdmin},
},
}, http.StatusOK, &resp)
require.NotNil(t, resp.User)
}
func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() {
t := s.T()