From 6d997bdfdaf2f17549d2877e60cdec3407ca3c99 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 15 Jun 2026 07:59:32 -0300 Subject: [PATCH] Add fleet-user creation check (#47566) - [X] QA'd all new/changed functionality manually ## 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. --- server/service/integration_enterprise_test.go | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index db02fbf9c1..e538f0a4a8 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -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()