Align self-managed Restricted access with User.billable
What does this MR do and why?
Restricted access decides whether a change consumes a new seat via invites - User.billable. Since !216727 (merged) (18.9), User.billable excludes Minimal Access users on Self-managed Premium, but the seat check never adopted the same exclusion, so it rejected non-billable changes with the error "Could not update role because there are no available seats in this group".
Changes in ee/lib/gitlab_subscriptions/member_management/block_seat_overages.rb:
sm_non_billable_member?: a target role of Minimal Access is now non-billable on all paid tiers, not only whenexclude_guests_from_active_count?is true (applies to Ultimate). Guest stays gated on that same check.seats_available_for_self_managed?:new_invitesno longer includes inactive accounts, viainactive_user_ids. They fall outsideUser.billablefor a different reason than a new user does - they hold no seat and changing their role consumes none.
This change is the Self-managed half only, as it will be backported. The SaaS path is unchanged in this MR but will be adapted in a follow-up MR.
Note that ee/app/services/ee/members/update_service.rb is not actually changed, despite the linked issue describing the second fix as living "in the update path". The fix sits in the shared GitlabSubscriptions::MemberManagement::BlockSeatOverages module. It affects every Self-managed caller of the seat check. Behaviour is covered by that module's own specs; Members::UpdateService has additional service-level coverage because that's where the reported error surfaces.
References
Screenshots or screen recordings
N/A
How to set up and validate locally
-
Start GDK in Self-managed mode with a Premium license
-
Ensure the billable count exceeds the license seat count
-
Turn on Restricted access
-
Verify in the rails console:
# Confirming the correct setup ::Gitlab::Saas.feature_available?(:gitlab_com_subscriptions) # => false License.current.plan # => "premium" License.current.seats < User.billable.count # => true bso = GitlabSubscriptions::MemberManagement::BlockSeatOverages # Fix 1: Minimal access is non billable on Premium bso.send(:sm_non_billable_member?, Gitlab::Access::MINIMAL_ACCESS, nil) # => true (master: false) bso.send(:sm_non_billable_member?, Gitlab::Access::GUEST, nil) # => false - Guest stays billable on Premium (master: also false) # Fix 2: An inactive account does not consume a seat group = Group.first user = User.active.without_bots.last User.billable.exists?(user.id) # => true - confirms they hold a seat today bso.seats_available_for?(group, [user.id], Gitlab::Access::DEVELOPER, nil) # => true - they already hold a seat (master: also true) user.deactivate! bso.seats_available_for?(group, [user.id], Gitlab::Access::DEVELOPER, nil) # => true (master: false)
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.