From 3d016c547a5af77625457f836e1cf6756d839553 Mon Sep 17 00:00:00 2001
From: Jason Goodman <jgoodman@gitlab.com>
Date: Wed, 31 Jan 2024 01:44:51 +0000
Subject: [PATCH] Allow new guest users exceeding instance user cap on ultimate
 license

This is behind a feature flag.
When the feature flag is enabled, new non-billable users will be set
to active even if they exceed the instance user cap.
With the feature flag disabled, any new human user, billable or not,
will be blocked pending approval if the instance user cap is exceeded.
---
 ee/app/models/ee/user.rb                      |  8 +++
 ee/app/services/ee/users/build_service.rb     | 15 +++++-
 ...nbillable_users_over_instance_user_cap.yml |  9 ++++
 ee/spec/models/ee/user_spec.rb                | 31 ++++++++++++
 .../ee/registrations_controller_spec.rb       | 23 +++++++++
 .../services/ee/users/build_service_spec.rb   | 50 +++++++++++++++++++
 6 files changed, 135 insertions(+), 1 deletion(-)
 create mode 100644 ee/config/feature_flags/wip/activate_nonbillable_users_over_instance_user_cap.yml

diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb
index 531e8ba670263..7dadf830de06a 100644
--- a/ee/app/models/ee/user.rb
+++ b/ee/app/models/ee/user.rb
@@ -252,6 +252,14 @@ def clear_group_with_ai_available_cache(ids)
       end
     end
 
+    def pending_billable_invitations
+      if ::License.current.exclude_guests_from_active_count?
+        pending_invitations.where('access_level > ?', ::Gitlab::Access::GUEST)
+      else
+        pending_invitations
+      end
+    end
+
     def external?
       return true if security_policy_bot?
 
diff --git a/ee/app/services/ee/users/build_service.rb b/ee/app/services/ee/users/build_service.rb
index 6bd9880b20e38..53129ecb336f2 100644
--- a/ee/app/services/ee/users/build_service.rb
+++ b/ee/app/services/ee/users/build_service.rb
@@ -117,10 +117,23 @@ def build_scim_identity
 
       def set_pending_approval_state
         return unless ::User.user_cap_reached?
-        return unless user.human?
+
+        if ::Feature.enabled?(:activate_nonbillable_users_over_instance_user_cap, type: :wip)
+          return unless will_be_billable?(user)
+        else
+          return unless user.human?
+        end
 
         user.state = ::User::BLOCKED_PENDING_APPROVAL_STATE
       end
+
+      def will_be_billable?(user)
+        user.human? && (all_humans_are_billable? || user.pending_billable_invitations.any?)
+      end
+
+      def all_humans_are_billable?
+        !::License.current.exclude_guests_from_active_count?
+      end
     end
   end
 end
diff --git a/ee/config/feature_flags/wip/activate_nonbillable_users_over_instance_user_cap.yml b/ee/config/feature_flags/wip/activate_nonbillable_users_over_instance_user_cap.yml
new file mode 100644
index 0000000000000..1233831da721e
--- /dev/null
+++ b/ee/config/feature_flags/wip/activate_nonbillable_users_over_instance_user_cap.yml
@@ -0,0 +1,9 @@
+---
+name: activate_nonbillable_users_over_instance_user_cap
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361563
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142468
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/439520
+milestone: '16.9'
+group: group::utilization
+type: wip
+default_enabled: false
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index a0d77b25d0bd9..948609daeb9f6 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -1296,6 +1296,37 @@
     end
   end
 
+  describe '#pending_billable_invitations' do
+    let_it_be(:user) { described_class.new(confirmed_at: Time.zone.now, email: 'test@example.com') }
+
+    it 'returns pending billable invitations for the user' do
+      invitation = create(:group_member, :guest, :invited, invite_email: user.email)
+
+      expect(user.pending_billable_invitations).to eq([invitation])
+    end
+
+    it 'returns both project and group invitations' do
+      project_invitation = create(:project_member, :maintainer, :invited, invite_email: user.email)
+      group_invitation = create(:group_member, :developer, :invited, invite_email: user.email)
+
+      expect(user.pending_billable_invitations).to contain_exactly(project_invitation, group_invitation)
+    end
+
+    context 'with an ultimate license' do
+      before do
+        license = create(:license, plan: License::ULTIMATE_PLAN)
+        allow(License).to receive(:current).and_return(license)
+      end
+
+      it 'excludes pending non-billable invitations for the user' do
+        create(:group_member, :guest, :invited, invite_email: user.email)
+        developer_invitation = create(:group_member, :developer, :invited, invite_email: user.email)
+
+        expect(user.pending_billable_invitations).to eq([developer_invitation])
+      end
+    end
+  end
+
   describe '#group_managed_account?' do
     subject { user.group_managed_account? }
 
diff --git a/ee/spec/requests/ee/registrations_controller_spec.rb b/ee/spec/requests/ee/registrations_controller_spec.rb
index a62e144959222..3e6ac0b398ca3 100644
--- a/ee/spec/requests/ee/registrations_controller_spec.rb
+++ b/ee/spec/requests/ee/registrations_controller_spec.rb
@@ -205,5 +205,28 @@
         create_user
       end
     end
+
+    describe 'user signup cap' do
+      before do
+        stub_application_setting(require_admin_approval_after_user_signup: false)
+      end
+
+      context 'when user signup cap is exceeded on an ultimate license' do
+        before do
+          allow(Gitlab::CurrentSettings).to receive(:new_user_signups_cap).and_return(1)
+          create(:group_member, :developer)
+
+          license = create(:license, plan: License::ULTIMATE_PLAN)
+          allow(License).to receive(:current).and_return(license)
+        end
+
+        it 'sets a new non-billable user state to active' do
+          create_user
+
+          user = User.find_by(email: user_attrs[:email])
+          expect(user).to be_active
+        end
+      end
+    end
   end
 end
diff --git a/ee/spec/services/ee/users/build_service_spec.rb b/ee/spec/services/ee/users/build_service_spec.rb
index 9dcf3fa78ade0..3f038915a00d3 100644
--- a/ee/spec/services/ee/users/build_service_spec.rb
+++ b/ee/spec/services/ee/users/build_service_spec.rb
@@ -158,6 +158,56 @@
               expect(user).to be_active
             end
           end
+
+          context 'with an ultimate license' do
+            let_it_be(:group) { create(:group) }
+            let_it_be(:billable_users) { create_list(:user, 3) }
+
+            before_all do
+              billable_users.each { |u| group.add_developer(u) }
+            end
+
+            before do
+              license = create(:license, plan: License::ULTIMATE_PLAN)
+              allow(License).to receive(:current).and_return(license)
+            end
+
+            it 'sets a new billable user state to blocked pending approval' do
+              member = create(:group_member, :developer, :invited)
+              params.merge!(email: member.invite_email, skip_confirmation: true)
+
+              user = service.execute
+
+              expect(user).to be_blocked_pending_approval
+            end
+
+            it 'sets a new non-billable user state to active' do
+              user = service.execute
+
+              expect(user).to be_active
+            end
+
+            context 'when the feature flag is disabled' do
+              before do
+                stub_feature_flags(activate_nonbillable_users_over_instance_user_cap: false)
+              end
+
+              it 'sets a new billable user state to blocked pending approval' do
+                member = create(:group_member, :developer, :invited)
+                params.merge!(email: member.invite_email, skip_confirmation: true)
+
+                user = service.execute
+
+                expect(user).to be_blocked_pending_approval
+              end
+
+              it 'sets a new non-billable user state to blocked pending approval' do
+                user = service.execute
+
+                expect(user).to be_blocked_pending_approval
+              end
+            end
+          end
         end
 
         context 'when user signup cap is not set' do
-- 
GitLab