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:

  1. sm_non_billable_member?: a target role of Minimal Access is now non-billable on all paid tiers, not only when exclude_guests_from_active_count? is true (applies to Ultimate). Guest stays gated on that same check.
  2. seats_available_for_self_managed?: new_invites no longer includes inactive accounts, via inactive_user_ids. They fall outside User.billable for 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

  1. Start GDK in Self-managed mode with a Premium license

  2. Ensure the billable count exceeds the license seat count

  3. Turn on Restricted access

  4. 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.

Edited by Katherine Richards

Merge request reports

Loading
Loading