From 61648c98a7f21985a7a579bc9ac859c85788e75e Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 23 Oct 2026 05:34:31 +0000 Subject: [PATCH] test(auth): pin that a self-edit adding a non-admin permission the actor lacks is refused The #226 self-promotion test covered only adding the Administrators group, so nothing committed proved the membership grant ceiling on a self-edit is general rather than an admin:* special case. A self-edit adding a group with an ordinary, non-carved-out permission the actor does not hold (here view:recommendations) passes guardSelfEscalation, since the actor holds update:users, nothing is carved out and no account scope widens. Only the grant ceiling, measured against the actor's prior permissions, refuses it. Add TestMembershipCeiling_UpdateUser_SelfEditNonAdminPermissionAboveCeilingRefused next to the existing membership ceiling tests. It asserts ErrPermissionCeiling (not ErrSelfEscalation), that the error names view:recommendations, and that UpdateUserAdminFields is never reached. Restricting the membership ceiling to admin:* only survives the rest of the internal/auth unit suite and is killed only by this test. Closes #419 Co-Authored-By: Claude Opus 5.5 --- internal/auth/membership_ceiling_test.go | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/internal/auth/membership_ceiling_test.go b/internal/auth/membership_ceiling_test.go index 2c7d99986..80541384c 100644 --- a/internal/auth/membership_ceiling_test.go +++ b/internal/auth/membership_ceiling_test.go @@ -111,6 +111,38 @@ func TestMembershipCeiling_UpdateUser_UpdateUsersOnlyCannotSelfPromoteToAdmin(t mockStore.AssertNotCalled(t, "UpdateUserAdminFields", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) } +// Non-admin variant of the self-promotion test above (issue #419): the +// ceiling is not an Administrators special case. An actor holding only +// update:users adds to their OWN membership a group carrying an ordinary, +// non-carved-out permission they do not hold. guardSelfEscalation admits it +// (update:users is held, nothing carved out, no account restriction), so +// only the #226 grant ceiling, measured against the actor's prior +// permissions, stands between the actor and view:recommendations. +func TestMembershipCeiling_UpdateUser_SelfEditNonAdminPermissionAboveCeilingRefused(t *testing.T) { + ctx := context.Background() + mockStore := new(MockStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) + svc := newCeilingService(t, mockStore) + + recommendationViewers := &Group{ + ID: "44444444-4444-4444-8444-000000000002", + Name: "Recommendation Viewers", + Permissions: []Permission{{Action: ActionView, Resource: ResourceRecommendations}}, + } + stubActorPermissions(ctx, mockStore, updateUsersOnly) + mockStore.On("GetGroup", ctx, recommendationViewers.ID).Return(recommendationViewers, nil) + + _, err := svc.UpdateUser(ctx, ceilingActorID, ceilingActorID, UpdateUserRequest{ + GroupIDs: []string{ceilingActorGroupID, recommendationViewers.ID}, + }) + + require.Error(t, err) + assert.ErrorIs(t, err, ErrPermissionCeiling) + assert.NotErrorIs(t, err, ErrSelfEscalation) + assert.Contains(t, err.Error(), ActionView+":"+ResourceRecommendations) + mockStore.AssertNotCalled(t, "UpdateUserAdminFields", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) +} + // Negative control: an actor may still create a user in a group whose // permissions their own effective set already covers. func TestMembershipCeiling_CreateUser_AllowedWithinCeiling(t *testing.T) {