diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index 531e8ba670263876dd6e89c5482f0a14719e3d8f..7dadf830de06a62e926264e12a9380f326685287 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 6bd9880b20e386edae1d6f3f3d477b3d25089ef3..53129ecb336f2c5d5b0c0854c6f05d5e8d6ac1d0 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 0000000000000000000000000000000000000000..1233831da721e204afd62ab083ed3598b4e10693 --- /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 a0d77b25d0bd9d70a80fc821a9685d3260ae2be2..948609daeb9f68b69e112ecc2d07a4f92ec9c5d5 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 a62e144959222d25bb2bbfd73f414925e854d646..3e6ac0b398ca3b56237bd8622383086186bc13e3 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 9dcf3fa78ade0fc26571b3c2d4e1bf0e933400ac..3f038915a00d3646463db4b38af0e5b684e2dcf1 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