Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +5127 to +5132
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)
Comment on lines +5180 to +5188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add persistence verification for the API-only forbidden request.

The admin endpoint test (lines 5176-5178) verifies the forbidden user was not persisted, but the API-only endpoint test has no equivalent check. While both funnel through the same authorization layer, asymmetric verification weakens regression protection.

✅ Add a persistence check after the API-only request

If the createAPIOnlyUserRequest includes an email or if API-only users can be queried by name, add a verification similar to lines 5176-5178:

 	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)
+
+	// Verify the forbidden API-only request also didn't persist a user.
+	// Adjust the query method based on how API-only users can be retrieved.
+	_, err = s.ds.UserByEmail(ctx, t.Name()+"_crossteam_api@example.com")
+	require.True(t, fleet.IsNotFound(err))

Note: Adjust the verification method if API-only users don't have email addresses or require a different lookup mechanism.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/integration_enterprise_test.go` around lines 5180 - 5188, The
API-only endpoint test calls s.DoJSON("POST",
"/api/latest/fleet/users/api_only", createAPIOnlyUserRequest...) and asserts
http.StatusForbidden but does not verify the user wasn't persisted; add a
persistence check after that call similar to the admin-path check: attempt to
lookup the user (by email if createAPIOnlyUserRequest included one, or by name
using the same query used earlier in the file) and assert the user does not
exist (or that the lookup returns empty/not found), using the same helper/query
used in the admin test to ensure symmetry with the admin endpoint verification;
if API-only users require a different lookup, use that mechanism instead.


// 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()

Expand Down
Loading