Reject last owner demotion
Return a conflict when membership role updates would demote the final active owner in an organization. Add an end-to-end regression that verifies the mutation fails and leaves the owner role intact. Signed-off-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
committed by
Bryan Frimin
parent
88eb340aba
commit
e9bcdc85f4
@@ -115,6 +115,109 @@ func TestUser_UpdateMembership(t *testing.T) {
|
|||||||
assert.Equal(t, "VIEWER", mutationResult.UpdateMembership.Membership.Role)
|
assert.Equal(t, "VIEWER", mutationResult.UpdateMembership.Membership.Role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUser_UpdateMembershipRejectsLastOwnerDemotion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
queryMembers := `
|
||||||
|
query($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on Organization {
|
||||||
|
members(first: 10) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
identity { id }
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var membersResult struct {
|
||||||
|
Node struct {
|
||||||
|
Members struct {
|
||||||
|
Edges []struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Identity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"identity"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"edges"`
|
||||||
|
} `json:"members"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.ExecuteConnect(queryMembers, map[string]any{
|
||||||
|
"id": owner.GetOrganizationID().String(),
|
||||||
|
}, &membersResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var ownerMembershipID string
|
||||||
|
for _, edge := range membersResult.Node.Members.Edges {
|
||||||
|
if edge.Node.Identity.ID == owner.GetUserID().String() {
|
||||||
|
ownerMembershipID = edge.Node.ID
|
||||||
|
assert.Equal(t, "OWNER", edge.Node.Role)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEmpty(t, ownerMembershipID, "Should find owner member")
|
||||||
|
|
||||||
|
mutation := `
|
||||||
|
mutation($input: UpdateMembershipInput!) {
|
||||||
|
updateMembership(input: $input) {
|
||||||
|
membership {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
err = owner.ExecuteConnect(mutation, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"membershipId": ownerMembershipID,
|
||||||
|
"role": "VIEWER",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
testutil.RequireErrorCode(t, err, "CONFLICT")
|
||||||
|
|
||||||
|
var gqlErrors testutil.GraphQLErrors
|
||||||
|
require.ErrorAs(t, err, &gqlErrors)
|
||||||
|
assert.Equal(t, "cannot demote last active owner", gqlErrors[0].Message)
|
||||||
|
|
||||||
|
queryMembership := `
|
||||||
|
query($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on Membership {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var membershipResult struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.ExecuteConnect(queryMembership, map[string]any{
|
||||||
|
"id": ownerMembershipID,
|
||||||
|
}, &membershipResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "OWNER", membershipResult.Node.Role)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUser_RemoveUser(t *testing.T) {
|
func TestUser_RemoveUser(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.Upd
|
|||||||
|
|
||||||
membership, err := r.iam.OrganizationService.UpdateMembership(ctx, input.OrganizationID, input.MembershipID, input.Role)
|
membership, err := r.iam.OrganizationService.UpdateMembership(ctx, input.OrganizationID, input.MembershipID, input.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if _, ok := errors.AsType[*iam.ErrLastActiveOwner](err); ok {
|
||||||
|
return nil, gqlutils.Conflictf(ctx, "cannot demote last active owner")
|
||||||
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user