diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index c9b4705a0e346d113021779b555f74fa31026b2e..1b792c7869f3cee27df09e78a99c7cbd63617c90 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -546,10 +546,14 @@ def block_seat_overages? ::Gitlab::Saas.feature_available?(:gitlab_com_subscriptions) end - def seats_available_for?(invites) + def seats_available_for?(user_ids) return true unless gitlab_subscription - gitlab_subscription.seats >= (billable_members_count + invites.count) + billable_ids = billed_user_ids[:user_ids].to_a + + new_user_ids = user_ids - billable_ids + + gitlab_subscription.seats >= (billable_ids.count + new_user_ids.count) end def calculate_reactive_cache diff --git a/ee/app/services/ee/members/create_service.rb b/ee/app/services/ee/members/create_service.rb index 4245fd3f9d63314d5ca7f993bcaf423d6a27bc55..719b075734259c6f22b098fd8bcbbae8cd623d30 100644 --- a/ee/app/services/ee/members/create_service.rb +++ b/ee/app/services/ee/members/create_service.rb @@ -52,7 +52,12 @@ def check_membership_lock! def check_seats! root_namespace = source.root_ancestor - return unless root_namespace.block_seat_overages? && !root_namespace.seats_available_for?(invites) + return unless root_namespace.block_seat_overages? + + # Work in progress. Handle email invites in https://gitlab.com/gitlab-org/gitlab/-/issues/443383. + invited_user_ids = invites.select { |i| i.to_i.to_s == i } + + return if root_namespace.seats_available_for?(invited_user_ids.map(&:to_i)) raise ::Members::CreateService::SeatLimitExceededError, s_('AddMember|Not enough seats for this many users.') end diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 6f762db449b459e40529349e1d0da383ef2f6fb0..195030940f84bc0ff4fdc40f000e9ae0cad2a55b 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -3626,6 +3626,7 @@ def webhook_headers describe '#seats_available_for?' do context 'with a subscription', :saas do let_it_be(:group, refind: true) { create(:group_with_plan, plan: :premium_plan) } + let_it_be(:user) { create(:user) } before_all do group.gitlab_subscription.update!(seats: 5) @@ -3651,19 +3652,26 @@ def webhook_headers it 'counts members in subgroups as consuming seats' do subgroup = create(:group, parent: group) - subgroup.add_developer(create(:user)) + subgroup.add_developer(user) user_ids = [1, 2, 3, 4, 5] expect(group.seats_available_for?(user_ids)).to eq(false) end - it 'returns true if passed an empty enumerable' do + it 'considers if users are already consuming a seat' do + group.gitlab_subscription.update!(seats: 1) + group.add_developer(user) + + expect(group.seats_available_for?([user.id])).to eq(true) + end + + it 'returns true if passed an empty array' do expect(group.seats_available_for?([])).to eq(true) end - it 'returns true if there are no seats remaining and the passed enumerable is empty' do + it 'returns true if there are no seats remaining and the passed array is empty' do group.gitlab_subscription.update!(seats: 1) - group.add_maintainer(create(:user)) + group.add_maintainer(user) expect(group.seats_available_for?([])).to eq(true) end diff --git a/ee/spec/requests/api/invitations_spec.rb b/ee/spec/requests/api/invitations_spec.rb index 7f7f08880ec18e0355e1108b94d0c6a85e3bfdde..afde90da659737bcd359f62c9fb93d43cfcea2f6 100644 --- a/ee/spec/requests/api/invitations_spec.rb +++ b/ee/spec/requests/api/invitations_spec.rb @@ -371,6 +371,17 @@ }) end + it 'adds the member when the member is already in the group when all the seats are taken' do + group.gitlab_subscription.update!(seats: 2) + group.add_guest(user) + + post api(url, owner), params: { access_level: Member::DEVELOPER, user_id: user.id } + + expect(project.members.flat_map { |m| [m.user_id, m.access_level] }).to eq([user.id, Member::DEVELOPER]) + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to eq({ 'status' => 'success' }) + end + context 'when the feature flag is disabled' do before do stub_feature_flags(block_seat_overages: false) diff --git a/ee/spec/services/ee/members/create_service_spec.rb b/ee/spec/services/ee/members/create_service_spec.rb index f21e3bd48c0af31d9b04795ccbfdd78e9ea11a96..42566fc6c383e86d781a8ca45c69fb2b6392b138 100644 --- a/ee/spec/services/ee/members/create_service_spec.rb +++ b/ee/spec/services/ee/members/create_service_spec.rb @@ -299,4 +299,29 @@ end end end + + context 'with block seat overages enabled', :saas do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group_with_plan, plan: :premium_plan) } + let_it_be(:project) { create(:project, group: group) } + + before_all do + project.add_maintainer(user) + end + + before do + stub_saas_features(gitlab_com_subscriptions: true) + stub_feature_flags(block_seat_overages: true) + end + + context 'with invited emails' do + let(:invites) { ['email@example.com'] } + + it 'removes invite emails from the seat check' do + group.gitlab_subscription.update!(seats: 1) + + expect { execute_service }.to change { project.members.count }.by(1) + end + end + end end