Require set-owner authorization to create OWNER membership
An organization ADMIN could mint an OWNER membership via createUser, which only gated iam:membership-profile:create and bypassed the owner-only iam:membership-role:set-owner check that updateMembership already enforces. Gate the requested role in both createUser entry points (connect resolver and the MCP CreateUserTool) with an additional set-owner authorization when the role is OWNER, mirroring updateMembership. Add a regression test that locks the ADMIN/OWNER privilege boundary the fix relies on. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -24,6 +24,77 @@ import (
|
||||
"go.probo.inc/probo/e2e/internal/testutil"
|
||||
)
|
||||
|
||||
// TestUser_AdminCannotCreateOwner is a non-regression test for the vertical
|
||||
// privilege escalation where an ADMIN could mint an OWNER membership via
|
||||
// createUser, bypassing the owner-only set-owner authorization that
|
||||
// updateMembership enforces.
|
||||
func TestUser_AdminCannotCreateOwner(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
|
||||
|
||||
const createUserMutation = `
|
||||
mutation($input: CreateUserInput!) {
|
||||
createUser(input: $input) {
|
||||
profileEdge {
|
||||
node {
|
||||
membership { role }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
newUserInput := func(role string) map[string]any {
|
||||
return map[string]any{
|
||||
"input": map[string]any{
|
||||
"organizationId": owner.GetOrganizationID().String(),
|
||||
"emailAddress": factory.SafeEmail(),
|
||||
"fullName": factory.SafeName("Privesc User"),
|
||||
"role": role,
|
||||
"kind": "EMPLOYEE",
|
||||
"additionalEmailAddresses": []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// An ADMIN must not be able to create an OWNER membership.
|
||||
_, err := admin.DoConnect(createUserMutation, newUserInput("OWNER"))
|
||||
testutil.RequireForbiddenError(t, err)
|
||||
|
||||
// The same ADMIN can still create a lower-privileged member.
|
||||
var adminCreate struct {
|
||||
CreateUser struct {
|
||||
ProfileEdge struct {
|
||||
Node struct {
|
||||
Membership struct {
|
||||
Role string `json:"role"`
|
||||
} `json:"membership"`
|
||||
} `json:"node"`
|
||||
} `json:"profileEdge"`
|
||||
} `json:"createUser"`
|
||||
}
|
||||
err = admin.ExecuteConnect(createUserMutation, newUserInput("ADMIN"), &adminCreate)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ADMIN", adminCreate.CreateUser.ProfileEdge.Node.Membership.Role)
|
||||
|
||||
// An OWNER remains able to create an OWNER membership.
|
||||
var ownerCreate struct {
|
||||
CreateUser struct {
|
||||
ProfileEdge struct {
|
||||
Node struct {
|
||||
Membership struct {
|
||||
Role string `json:"role"`
|
||||
} `json:"membership"`
|
||||
} `json:"node"`
|
||||
} `json:"profileEdge"`
|
||||
} `json:"createUser"`
|
||||
}
|
||||
err = owner.ExecuteConnect(createUserMutation, newUserInput("OWNER"), &ownerCreate)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OWNER", ownerCreate.CreateUser.ProfileEdge.Node.Membership.Role)
|
||||
}
|
||||
|
||||
func TestUser_UpdateMembership(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
|
||||
@@ -26,6 +26,12 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUse
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if input.Role == coredata.MembershipRoleOwner {
|
||||
if _, err := r.authorize(ctx, input.OrganizationID, iam.ActionMembershipRoleSetOwner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
profile, err := r.iam.OrganizationService.CreateUser(
|
||||
ctx,
|
||||
&iam.CreateUserRequest{
|
||||
|
||||
@@ -2819,6 +2819,12 @@ func (r *Resolver) CreateUserTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
return nil, types.CreateUserOutput{}, err
|
||||
}
|
||||
|
||||
if input.Role == coredata.MembershipRoleOwner {
|
||||
if _, err := r.Authorize(ctx, input.OrganizationID, iam.ActionMembershipRoleSetOwner); err != nil {
|
||||
return nil, types.CreateUserOutput{}, err
|
||||
}
|
||||
}
|
||||
|
||||
var contractStart, contractEnd **time.Time
|
||||
if input.ContractStartDate != nil {
|
||||
contractStart = &input.ContractStartDate
|
||||
|
||||
Reference in New Issue
Block a user