From 6b45d743758a3f333dde7eff18b14fd862c2970c Mon Sep 17 00:00:00 2001 From: smriti Date: Thu, 27 Nov 2025 15:35:45 +0530 Subject: [PATCH 1/6] Changes for service accounts to be created for subgroups --- .../groups/settings/service_accounts_controller.rb | 5 ----- ee/app/policies/ee/group_policy.rb | 6 ------ ee/lib/ee/sidebars/groups/menus/settings_menu.rb | 3 +-- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/ee/app/controllers/groups/settings/service_accounts_controller.rb b/ee/app/controllers/groups/settings/service_accounts_controller.rb index 8e04b79b68be8c..fb6d479b7a18a1 100644 --- a/ee/app/controllers/groups/settings/service_accounts_controller.rb +++ b/ee/app/controllers/groups/settings/service_accounts_controller.rb @@ -7,7 +7,6 @@ class ServiceAccountsController < Groups::ApplicationController feature_category :user_management - before_action :ensure_root_group! before_action :authorize_admin_service_accounts!, except: [:index, :show] before_action :authorize_read_service_accounts!, only: [:index, :show] @@ -25,10 +24,6 @@ def authorize_admin_service_accounts! render_404 unless can?(current_user, :create_service_account, group) && can?(current_user, :delete_service_account, group) end - - def ensure_root_group! - render_404 unless group.root? - end end end end diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 94719f5e4862a9..65becff207f907 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -631,12 +631,6 @@ module GroupPolicy prevent :read_service_account end - rule { has_parent }.policy do - prevent :read_service_account - prevent :create_service_account - prevent :delete_service_account - end - rule { ~service_accounts_available }.policy do prevent :read_service_account prevent :admin_service_accounts diff --git a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb index e84a62a4814718..356da1f54a2727 100644 --- a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb +++ b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb @@ -74,8 +74,7 @@ def roles_and_permissions_menu_item end def service_accounts_available? - context.group.root? && - can?(context.current_user, :read_service_account, context.group) + can?(context.current_user, :read_service_account, context.group) end def custom_roles_enabled? -- GitLab From b8e26d0d2719d8e860207eea6cfd480a1e831558 Mon Sep 17 00:00:00 2001 From: smriti Date: Thu, 27 Nov 2025 20:39:34 +0530 Subject: [PATCH 2/6] Specs fixed --- .../groups/menus/settings_menu_spec.rb | 2 +- ee/spec/policies/group_policy_spec.rb | 68 ++++++++----------- .../service_accounts_controller_spec.rb | 2 +- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb index 8b1f9e622854f5..32b75fbcecbf52 100644 --- a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb @@ -46,7 +46,7 @@ let(:container) { subgroup } - it { is_expected.not_to be_present } + it { is_expected.to be_present } end context 'when service accounts feature is not included in the license' do diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index cd7bc6e36a8222..00fc98326f6343 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -3695,50 +3695,40 @@ def expect_private_group_permissions_as_if_non_member it { is_expected.to be_allowed(:admin_service_accounts) } it { is_expected.to be_allowed(:admin_service_account_member) } - it { is_expected.to be_disallowed(:create_service_account) } - it { is_expected.to be_disallowed(:delete_service_account) } - it { is_expected.to be_disallowed(:read_service_account) } - end - end - - context 'for subgroup' do - subject { described_class.new(current_user, subgroup) } - - it { is_expected.to be_allowed(:admin_service_accounts) } - it { is_expected.to be_allowed(:admin_service_account_member) } - it { is_expected.to be_disallowed(:create_service_account) } - it { is_expected.to be_disallowed(:delete_service_account) } - it { is_expected.to be_disallowed(:read_service_account) } - - context 'when a trial is active in GitLab.com', :saas do - let(:current_user) { owner } + it { is_expected.to be_allowed(:create_service_account) } + it { is_expected.to be_allowed(:delete_service_account) } + it { is_expected.to be_allowed(:read_service_account) } - before do - allow(subgroup.root_ancestor).to receive_messages(trial_active?: true) - end + context 'when a trial is active in GitLab.com', :saas do + let(:current_user) { owner } - context 'for owner with verified identity' do before do - allow(owner).to receive(:identity_verified?).and_return(true) + allow(subgroup.root_ancestor).to receive_messages(trial_active?: true) end - it { is_expected.to be_allowed(:admin_service_accounts) } - it { is_expected.to be_allowed(:admin_service_account_member) } - it { is_expected.to be_disallowed(:create_service_account) } - it { is_expected.to be_disallowed(:delete_service_account) } - it { is_expected.to be_disallowed(:read_service_account) } - end + context 'for owner with verified identity' do + before do + allow(owner).to receive(:identity_verified?).and_return(true) + end - context 'for owner without verified identity' do - before do - allow(owner).to receive(:identity_verified?).and_return(false) + it { is_expected.to be_allowed(:admin_service_accounts) } + it { is_expected.to be_allowed(:admin_service_account_member) } + it { is_expected.to be_allowed(:create_service_account) } + it { is_expected.to be_allowed(:delete_service_account) } + it { is_expected.to be_allowed(:read_service_account) } end - it { is_expected.to be_disallowed(:admin_service_accounts) } - it { is_expected.to be_disallowed(:admin_service_account_member) } - it { is_expected.to be_disallowed(:create_service_account) } - it { is_expected.to be_disallowed(:delete_service_account) } - it { is_expected.to be_disallowed(:read_service_account) } + context 'for owner without verified identity' do + before do + allow(owner).to receive(:identity_verified?).and_return(false) + end + + it { is_expected.to be_disallowed(:admin_service_accounts) } + it { is_expected.to be_disallowed(:admin_service_account_member) } + it { is_expected.to be_disallowed(:create_service_account) } + it { is_expected.to be_disallowed(:delete_service_account) } + it { is_expected.to be_allowed(:read_service_account) } + end end end end @@ -3771,9 +3761,9 @@ def expect_private_group_permissions_as_if_non_member it { is_expected.to be_allowed(:admin_service_accounts) } it { is_expected.to be_allowed(:admin_service_account_member) } - it { is_expected.to be_disallowed(:create_service_account) } - it { is_expected.to be_disallowed(:delete_service_account) } - it { is_expected.to be_disallowed(:read_service_account) } + it { is_expected.to be_allowed(:create_service_account) } + it { is_expected.to be_allowed(:delete_service_account) } + it { is_expected.to be_allowed(:read_service_account) } end end diff --git a/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb b/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb index bec532620e922e..11c349eb8618a4 100644 --- a/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb +++ b/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb @@ -50,7 +50,7 @@ context 'when accessing a subgroup' do let_it_be(:group) { create(:group, parent: group) } - it_behaves_like 'page is not found' + it_behaves_like 'page is found' end context 'when license is disabled' do -- GitLab From 5dc1309d0752c657cc92bd462c4f18b88c14f739 Mon Sep 17 00:00:00 2001 From: smriti Date: Thu, 27 Nov 2025 20:46:34 +0530 Subject: [PATCH 3/6] Changes finalized behind feature flag subscription changes finalized --- .../groups/settings/service_accounts_controller.rb | 10 ++++++++++ ee/app/policies/ee/group_policy.rb | 10 ++++++++++ .../allow_subgroups_to_create_service_accounts.yml | 10 ++++++++++ ee/lib/ee/sidebars/groups/menus/settings_menu.rb | 7 ++++++- .../ee/sidebars/groups/menus/settings_menu_spec.rb | 9 +++++++++ ee/spec/policies/group_policy_spec.rb | 13 +++++++++++++ ee/spec/requests/api/group_service_accounts_spec.rb | 13 ------------- .../settings/service_accounts_controller_spec.rb | 8 ++++++++ 8 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml diff --git a/ee/app/controllers/groups/settings/service_accounts_controller.rb b/ee/app/controllers/groups/settings/service_accounts_controller.rb index fb6d479b7a18a1..4a126966ead39d 100644 --- a/ee/app/controllers/groups/settings/service_accounts_controller.rb +++ b/ee/app/controllers/groups/settings/service_accounts_controller.rb @@ -7,6 +7,8 @@ class ServiceAccountsController < Groups::ApplicationController feature_category :user_management + before_action :ensure_root_group! + before_action :authorize_admin_service_accounts!, except: [:index, :show] before_action :authorize_read_service_accounts!, only: [:index, :show] @@ -16,6 +18,14 @@ class ServiceAccountsController < Groups::ApplicationController private + def ensure_root_group! + return if ::Feature.enabled?(:allow_subgroups_to_create_service_accounts, group) + + return if group.root? + + render_404 + end + def authorize_read_service_accounts! render_404 unless can?(current_user, :read_service_account, group) end diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 65becff207f907..7629ecc5cfa8a8 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -214,6 +214,10 @@ module GroupPolicy end end + condition(:service_accounts_enabled_for_subgroups) do + ::Feature.enabled?(:allow_subgroups_to_create_service_accounts, @subject) + end + condition(:service_accounts_available) do if @subject.root_ancestor.trial_active? @subject.feature_available?(:service_accounts) @@ -631,6 +635,12 @@ module GroupPolicy prevent :read_service_account end + rule { has_parent & ~service_accounts_enabled_for_subgroups }.policy do + prevent :read_service_account + prevent :create_service_account + prevent :delete_service_account + end + rule { ~service_accounts_available }.policy do prevent :read_service_account prevent :admin_service_accounts diff --git a/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml b/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml new file mode 100644 index 00000000000000..e40e81f17a1405 --- /dev/null +++ b/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml @@ -0,0 +1,10 @@ +--- +name: allow_subgroups_to_create_service_accounts +description: Service accounts should be allowed to be created for subgroups +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/work_items/540777 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/214373 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/582313 +milestone: '18.7' +group: group::authentication +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb index 356da1f54a2727..b64addffcfe665 100644 --- a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb +++ b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb @@ -74,7 +74,12 @@ def roles_and_permissions_menu_item end def service_accounts_available? - can?(context.current_user, :read_service_account, context.group) + if ::Feature.enabled?(:allow_subgroups_to_create_service_accounts, context.group) + can?(context.current_user, :read_service_account, context.group) + else + context.group.root? && + can?(context.current_user, :read_service_account, context.group) + end end def custom_roles_enabled? diff --git a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb index 32b75fbcecbf52..d77141346ee3c6 100644 --- a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb @@ -47,6 +47,15 @@ let(:container) { subgroup } it { is_expected.to be_present } + + context 'when feature flag allow_subgroups_to_create_service_accounts is false' do + before do + stub_feature_flags(allow_subgroups_to_create_service_accounts: false) + allow(::Feature).to receive(:enabled?).and_call_original + end + + it { is_expected.not_to be_present } + end end context 'when service accounts feature is not included in the license' do diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 00fc98326f6343..41118bf4d0496e 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -3729,6 +3729,19 @@ def expect_private_group_permissions_as_if_non_member it { is_expected.to be_disallowed(:delete_service_account) } it { is_expected.to be_allowed(:read_service_account) } end + + context 'when feature flag allow_subgroups_to_create_service_accounts is false' do + before do + allow(owner).to receive(:identity_verified?).and_return(true) + stub_feature_flags(allow_subgroups_to_create_service_accounts: false) + end + + it { is_expected.to be_allowed(:admin_service_accounts) } + it { is_expected.to be_allowed(:admin_service_account_member) } + it { is_expected.to be_disallowed(:create_service_account) } + it { is_expected.to be_disallowed(:delete_service_account) } + it { is_expected.to be_disallowed(:read_service_account) } + end end end end diff --git a/ee/spec/requests/api/group_service_accounts_spec.rb b/ee/spec/requests/api/group_service_accounts_spec.rb index c731f4ea31178c..efa438acfb5332 100644 --- a/ee/spec/requests/api/group_service_accounts_spec.rb +++ b/ee/spec/requests/api/group_service_accounts_spec.rb @@ -216,19 +216,6 @@ expect(response).to have_gitlab_http_status(:bad_request) end - - context 'for subgroup' do - let(:group_id) { subgroup.id } - - it 'returns error' do - perform_request - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include( - s_('ServiceAccount|User does not have permission to create a service account in this namespace.') - ) - end - end end context 'when the group does not exist' do diff --git a/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb b/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb index 11c349eb8618a4..774bdbe4c85c52 100644 --- a/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb +++ b/ee/spec/requests/groups/settings/service_accounts_controller_spec.rb @@ -51,6 +51,14 @@ let_it_be(:group) { create(:group, parent: group) } it_behaves_like 'page is found' + + context 'when feature flag allow_subgroups_to_create_service_accounts is false' do + before do + stub_feature_flags(allow_subgroups_to_create_service_accounts: false) + end + + it_behaves_like 'page is not found' + end end context 'when license is disabled' do -- GitLab From 36a2106be3c272ac4915d3b5d4ef0ff6b4b690a5 Mon Sep 17 00:00:00 2001 From: smriti Date: Tue, 9 Dec 2025 17:31:51 +0530 Subject: [PATCH 4/6] Added feature flag check for subgroup seats available check Midway commit Linting issues fixed --- doc/user/profile/service_accounts.md | 5 +- .../service_accounts/create_service.rb | 23 +- .../users/service_accounts/create_service.rb | 12 +- .../api/group_service_accounts_spec.rb | 30 +++ .../service_accounts/create_service_spec.rb | 244 ++++++++---------- .../service_accounts/create_service_spec.rb | 54 +--- 6 files changed, 156 insertions(+), 212 deletions(-) diff --git a/doc/user/profile/service_accounts.md b/doc/user/profile/service_accounts.md index d09e1a819a4b40..74450aee9c905e 100644 --- a/doc/user/profile/service_accounts.md +++ b/doc/user/profile/service_accounts.md @@ -40,7 +40,7 @@ Service accounts: - Do not receive notification emails without [adding a custom email address](../../api/service_accounts.md#create-an-instance-service-account). - Are not [billable users](../../subscriptions/manage_users_and_seats.md#billable-users) or [internal users](../../administration/internal_users.md). - Are available for [trial versions](https://gitlab.com/-/trial_registrations/new?glm_source=docs.gitlab.com&glm_content=free-user-limit-faq/ee/user/free_user_limit.html) - of GitLab.com after the Owner of the top-level group verifies their identity. Trial versions are limited to a maximum of 100 service accounts. + of GitLab.com after the Owner of the top-level group verifies their identity. - Can be used with trial versions of GitLab Self-Managed and GitLab Dedicated. You can also manage service accounts through the [service accounts API](../../api/service_accounts.md). @@ -108,8 +108,7 @@ to allow top-level group Owners to create group service accounts. The number of service accounts you can create is limited by your license: - On GitLab Free, you cannot create service accounts. -- On GitLab Premium, you can create one service account for every paid seat. -- On GitLab Ultimate, you can create an unlimited number of service accounts. +- On GitLab Ultimate and Premium, you can create an unlimited number of service accounts. To create a service account: diff --git a/ee/app/services/namespaces/service_accounts/create_service.rb b/ee/app/services/namespaces/service_accounts/create_service.rb index b00e85e921219a..75355e994dc709 100644 --- a/ee/app/services/namespaces/service_accounts/create_service.rb +++ b/ee/app/services/namespaces/service_accounts/create_service.rb @@ -72,27 +72,16 @@ def can_create_service_account? can?(current_user, :create_service_account, namespace) end - override :ultimate? - def ultimate? + override :active_subscription? + def active_subscription? return super unless saas? - return false if namespace.trial? # hosted plan can be ultimate even if a group is on trial - plan_name = namespace.actual_plan_name - [::Plan::GOLD, ::Plan::ULTIMATE].include?(plan_name) - end - - override :seats_available? - def seats_available? - return super unless saas? - return true if ultimate? + group_subscription = namespace.root_ancestor.gitlab_subscription - limit = if namespace.trial_active? - GitlabSubscription::SERVICE_ACCOUNT_LIMIT_FOR_TRIAL - else - namespace.gitlab_subscription.seats - end + return false unless group_subscription + return false if group_subscription&.expired? - limit > namespace.provisioned_users.service_accounts_without_composite_identity.count + Plan::PAID_HOSTED_PLANS.include?(group_subscription.plan_name) || namespace.trial_active? end def username_unavailable?(username) diff --git a/ee/app/services/users/service_accounts/create_service.rb b/ee/app/services/users/service_accounts/create_service.rb index 1b477318782cff..344be2927881b7 100644 --- a/ee/app/services/users/service_accounts/create_service.rb +++ b/ee/app/services/users/service_accounts/create_service.rb @@ -15,7 +15,7 @@ def initialize(current_user, params = {}) def execute return error(error_messages[:no_permission], :forbidden) unless can_create_service_account? - return error(error_messages[:no_seats], :forbidden) unless ultimate? || seats_available? + return error(error_messages[:no_seats], :forbidden) unless active_subscription? create_user end @@ -98,14 +98,8 @@ def error(message, reason) ServiceResponse.error(message: message, reason: reason) end - def ultimate? - License.current.ultimate? - end - - def seats_available? - return true if ultimate? - - User.service_accounts_without_composite_identity.count < License.current.seats.to_i + def active_subscription? + License.current.ultimate? || License.current.premium? || License.current.seats.to_i > 0 end end end diff --git a/ee/spec/requests/api/group_service_accounts_spec.rb b/ee/spec/requests/api/group_service_accounts_spec.rb index efa438acfb5332..1e56e1083d3c73 100644 --- a/ee/spec/requests/api/group_service_accounts_spec.rb +++ b/ee/spec/requests/api/group_service_accounts_spec.rb @@ -216,6 +216,36 @@ expect(response).to have_gitlab_http_status(:bad_request) end + + context 'for subgroup' do + let(:group_id) { subgroup.id } + + context 'when feature flag allow_subgroups_to_create_service_accounts is false' do + before do + stub_feature_flags(allow_subgroups_to_create_service_accounts: false) + end + + it 'returns error' do + perform_request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include( + s_('ServiceAccount|User does not have permission to create a service account in this namespace.') + ) + end + end + + it 'creates the user with the correct attributes' do + perform_request + + user = User.find(json_response['id']) + + expect(user.namespace.organization).to eq(current_organization) + expect(user.user_type).to eq('service_account') + expect(user.provisioned_by_group_id).to eq(group_id) + expect(user).to be_confirmed + end + end end context 'when the group does not exist' do diff --git a/ee/spec/services/namespaces/service_accounts/create_service_spec.rb b/ee/spec/services/namespaces/service_accounts/create_service_spec.rb index a3782048b3ab72..13d9a71ed3977d 100644 --- a/ee/spec/services/namespaces/service_accounts/create_service_spec.rb +++ b/ee/spec/services/namespaces/service_accounts/create_service_spec.rb @@ -22,7 +22,13 @@ context 'when the group is subgroup' do let(:namespace_id) { subgroup.id } - it_behaves_like 'service account creation failure' + context 'when feature flag allow_subgroups_to_create_service_accounts is false' do + before do + stub_feature_flags(allow_subgroups_to_create_service_accounts: false) + end + + it_behaves_like 'service account creation failure' + end end end @@ -153,57 +159,16 @@ context 'when subscription is of premium tier' do let(:license) { create(:license, plan: License::PREMIUM_PLAN) } - let_it_be(:service_account_without_composite) do - create(:user, :service_account, provisioned_by_group_id: group.id, composite_identity_enforced: false) - end - let_it_be(:service_account_without_composite2) do - create(:user, :service_account, provisioned_by_group_id: group.id, composite_identity_enforced: false) - end - - let_it_be(:service_account_with_composite) do - create(:user, :service_account, provisioned_by_group_id: group.id, composite_identity_enforced: true) - end - - context 'when premium seats are not available' do - before do - allow(license).to receive(:seats).and_return(1) - end - - it 'raises error' do - expect(result.status).to eq(:error) - expect(result.message).to include('No more seats are available to create Service Account User') - end + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account_group_#{group.id}" } end - context 'when premium seats are available' do - before do - allow(license).to receive(:seats).and_return(User.service_accounts_without_composite_identity.count + 2) - end - - it_behaves_like 'service account creation success' do - let(:username_prefix) { "service_account_group_#{group.id}" } - end - - it 'sets provisioned by group' do - expect(result.payload[:user].provisioned_by_group_id).to eq(group.id) - end - - it_behaves_like 'invalid namespace scenarios' + it 'sets provisioned by group' do + expect(result.payload[:user].provisioned_by_group_id).to eq(group.id) end - context 'when service accounts with composite_identity_enforced exist' do - it 'does not count them toward the seat limit' do - # We have 2 service accounts with composite_identity_enforced: false - # and 1 service account with composite_identity_enforced: true (AI Agent) - # If we set seats to 3, we should be able to create 1 more (since only 2 without composite count) - allow(license).to receive(:seats).and_return(3) - - result = service.execute - - expect(result.status).to eq(:success) - end - end + it_behaves_like 'invalid namespace scenarios' end end @@ -245,7 +210,7 @@ end context 'when gitlab_com_subscriptions saas feature is available', :saas do - let_it_be(:group) { create(:group_with_plan, owners: current_user) } + let_it_be(:group) { create(:group_with_plan, plan: :premium_plan, owners: current_user) } before do stub_saas_features(gitlab_com_subscriptions: true) @@ -254,20 +219,30 @@ it_behaves_like 'service account creation success' do let(:username_prefix) { "service_account_group_#{group.id}" } end - end - # setting is only applicable for top level group - context 'when the group is subgroup' do - let(:namespace_id) { subgroup.id } + context 'when the group is subgroup' do + let_it_be(:subgroup) { create(:group, :private, parent: group) } + let_it_be(:namespace_id) { subgroup.id } - it_behaves_like 'service account creation failure' + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account_group_#{namespace_id}" } + end - context 'when gitlab_com_subscriptions saas feature is available' do - before do - stub_saas_features(gitlab_com_subscriptions: true) + context 'when feature flag allow_subgroups_to_create_service_accounts is false' do + before do + stub_feature_flags(allow_subgroups_to_create_service_accounts: false) + end + + it_behaves_like 'service account creation failure' end + end + end - it_behaves_like 'service account creation failure' + context 'when the group is subgroup' do + let(:namespace_id) { subgroup.id } + + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account_group_#{namespace_id}" } end end end @@ -289,7 +264,6 @@ let(:namespace_id) { group_with_gold.id } before do - # Reflecting real production data: Gold subscriptions have 0 seats create(:gitlab_subscription, :gold, namespace: group_with_gold, seats: 0) end @@ -322,6 +296,53 @@ it_behaves_like 'invalid namespace scenarios' end + + context 'when subscription is of premium tier' do + let_it_be(:group_with_premium) { create(:group) } + let(:namespace_id) { group_with_premium.id } + + before do + create(:gitlab_subscription, :premium, namespace: group_with_premium) + end + + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account_group_#{group_with_premium.id}" } + end + + it 'sets provisioned by group' do + expect(result.payload[:user].provisioned_by_group_id).to eq(group_with_premium.id) + end + + it_behaves_like 'invalid namespace scenarios' + end + + context 'when subscription has expired' do + let_it_be(:group_with_expired) { create(:group) } + let(:namespace_id) { group_with_expired.id } + + before do + create(:gitlab_subscription, :ultimate, :expired, namespace: group_with_expired) + end + + it 'produces an error' do + expect(result.status).to eq(:error) + expect(result.message).to include('No more seats are available to create Service Account User') + end + end + + context 'when namespace has free plan' do + let_it_be(:group_with_free) { create(:group) } + let(:namespace_id) { group_with_free.id } + + before do + create(:gitlab_subscription, :free, namespace: group_with_free) + end + + it 'produces an error' do + expect(result.status).to eq(:error) + expect(result.message).to include('No more seats are available to create Service Account User') + end + end end context 'when current user is a group owner' do @@ -338,94 +359,43 @@ create(:gitlab_subscription, :active_trial, namespace: group_with_trial, hosted_plan: create(:ultimate_plan)) end - context 'when trial service account limit is not reached' do - before do - create_list(:user, 5, :service_account, provisioned_by_group_id: group_with_trial.id, - composite_identity_enforced: false) - create_list(:user, 5, :service_account, provisioned_by_group_id: group_with_trial.id, - composite_identity_enforced: true) - end - - it_behaves_like 'service account creation success' do - let(:username_prefix) { "service_account_group_#{group_with_trial.id}" } - end - - it 'sets provisioned by group' do - expect(result.payload[:user].provisioned_by_group_id).to eq(group_with_trial.id) - end - - it 'does not count service accounts with composite_identity_enforced toward limit' do - # We have 5 service accounts without composite and 5 with composite - # Only the 5 without composite should count toward the limit of 100 - expect(group_with_trial.provisioned_users.service_accounts_without_composite_identity.count).to eq(5) - end + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account_group_#{group_with_trial.id}" } end - context 'when trial service account limit is reached' do - before do - stub_const('GitlabSubscription::SERVICE_ACCOUNT_LIMIT_FOR_TRIAL', 3) - - create_list(:user, GitlabSubscription::SERVICE_ACCOUNT_LIMIT_FOR_TRIAL, :service_account, - provisioned_by_group_id: group_with_trial.id, composite_identity_enforced: false) - create_list(:user, 5, :service_account, - provisioned_by_group_id: group_with_trial.id, composite_identity_enforced: true) - end - - it 'produces an error' do - expect(result.status).to eq(:error) - expect(result.message).to include('No more seats are available to create Service Account User') - end - - it 'does not count service accounts with composite_identity_enforced toward limit' do - # We have 3 service accounts without composite (at limit) and 5 with composite - # Only the 3 without composite should count toward the limit - expect(group_with_trial.provisioned_users.service_accounts_without_composite_identity.count).to eq(3) - end + it 'sets provisioned by group' do + expect(result.payload[:user].provisioned_by_group_id).to eq(group_with_trial.id) end + end - context 'when subscription trial has expired' do - let_it_be(:group_with_expired_trial) { create(:group, owners: current_user) } - let(:namespace_id) { group_with_expired_trial.id } + context 'when subscription has expired' do + let_it_be(:group_with_expired_subscription) { create(:group, owners: current_user) } + let(:namespace_id) { group_with_expired_subscription.id } - before do - create(:gitlab_subscription, - namespace: group_with_expired_trial, - hosted_plan: create(:premium_plan), - trial: true, - trial_starts_on: 2.weeks.ago, - trial_ends_on: 1.day.ago, - seats: 5) - - create_list(:user, 2, :service_account, provisioned_by_group_id: group_with_expired_trial.id, - composite_identity_enforced: false) - create_list(:user, 1, :service_account, provisioned_by_group_id: group_with_expired_trial.id, - composite_identity_enforced: true) - end + before do + create(:gitlab_subscription, + namespace: group_with_expired_subscription, + hosted_plan: create(:premium_plan), + trial: false, + end_date: 1.day.ago) + end - context 'when regular subscription seats are available' do - it_behaves_like 'service account creation success' do - let(:username_prefix) { "service_account_group_#{group_with_expired_trial.id}" } - end + it 'produces an error' do + expect(result.status).to eq(:error) + expect(result.message).to include('No more seats are available to create Service Account User') + end + end - it 'does not count service accounts with composite_identity_enforced toward limit' do - # We have 2 service accounts without composite and 1 with composite - # Only the 2 without composite should count toward the limit - expect( - group_with_expired_trial.provisioned_users.service_accounts_without_composite_identity.count - ).to eq(2) - end - end + context 'when namespace has an active paid subscription' do + let_it_be(:group_with_paid) { create(:group, owners: current_user) } + let(:namespace_id) { group_with_paid.id } - context 'when regular subscription seats are not available' do - before do - group_with_expired_trial.gitlab_subscription.update!(seats: 2) - end + before do + create(:gitlab_subscription, :premium, namespace: group_with_paid) + end - it 'produces an error' do - expect(result.status).to eq(:error) - expect(result.message).to include('No more seats are available to create Service Account User') - end - end + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account_group_#{group_with_paid.id}" } end end end diff --git a/ee/spec/services/users/service_accounts/create_service_spec.rb b/ee/spec/services/users/service_accounts/create_service_spec.rb index fd4a7d6006e80c..7a4f0ecaf9634c 100644 --- a/ee/spec/services/users/service_accounts/create_service_spec.rb +++ b/ee/spec/services/users/service_accounts/create_service_spec.rb @@ -61,58 +61,20 @@ context 'when subscription is of premium tier' do let(:license) { create(:license, plan: License::PREMIUM_PLAN) } - let!(:service_account_without_composite) { create(:user, :service_account, composite_identity_enforced: false) } - let!(:service_account_without_composite2) do - create(:user, :service_account, composite_identity_enforced: false) - end - - let!(:service_account_with_composite) { create(:user, :service_account, composite_identity_enforced: true) } - - context 'when premium seats are not available' do - before do - allow(license).to receive(:seats).and_return(1) - end - it 'raises error' do - result = service.execute - - expect(result.status).to eq(:error) - expect(result.message).to include('No more seats are available to create Service Account User') - end + it_behaves_like 'service account creation success' do + let(:username_prefix) { "service_account" } end - context 'when premium seats are available' do - before do - allow(license).to receive(:seats).and_return(User.service_accounts_without_composite_identity.count + 2) - end - - it_behaves_like 'service account creation success' do - let(:username_prefix) { "service_account" } - end - - it_behaves_like 'service account creation with customized params' - - it 'correctly returns active model errors' do - service.execute - - result = service.execute - - expect(result.status).to eq(:error) - expect(result.message).to eq('Email has already been taken and Username has already been taken') - end - end + it_behaves_like 'service account creation with customized params' - context 'when service accounts with composite_identity_enforced exist' do - it 'does not count them toward the seat limit' do - # We have 2 service accounts with composite_identity_enforced: false - # and 1 service account with composite_identity_enforced: true (AI Agent) - # If we set seats to 3, we should be able to create 1 more (since only 2 without composite count) - allow(license).to receive(:seats).and_return(3) + it 'correctly returns active model errors' do + service.execute - result = service.execute + result = service.execute - expect(result.status).to eq(:success) - end + expect(result.status).to eq(:error) + expect(result.message).to eq('Email has already been taken and Username has already been taken') end end end -- GitLab From fcf32cf423da3380904c5e317fcae79b7cd8a30b Mon Sep 17 00:00:00 2001 From: smriti Date: Tue, 16 Dec 2025 21:25:17 +0530 Subject: [PATCH 5/6] Duo suggestions applied --- ee/app/services/namespaces/service_accounts/create_service.rb | 2 +- .../allow_subgroups_to_create_service_accounts.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/app/services/namespaces/service_accounts/create_service.rb b/ee/app/services/namespaces/service_accounts/create_service.rb index 75355e994dc709..017ed3f6999597 100644 --- a/ee/app/services/namespaces/service_accounts/create_service.rb +++ b/ee/app/services/namespaces/service_accounts/create_service.rb @@ -79,7 +79,7 @@ def active_subscription? group_subscription = namespace.root_ancestor.gitlab_subscription return false unless group_subscription - return false if group_subscription&.expired? + return false if group_subscription.expired? Plan::PAID_HOSTED_PLANS.include?(group_subscription.plan_name) || namespace.trial_active? end diff --git a/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml b/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml index e40e81f17a1405..87324bd4e729f5 100644 --- a/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml +++ b/ee/config/feature_flags/gitlab_com_derisk/allow_subgroups_to_create_service_accounts.yml @@ -4,7 +4,7 @@ description: Service accounts should be allowed to be created for subgroups feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/work_items/540777 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/214373 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/582313 -milestone: '18.7' +milestone: '18.8' group: group::authentication type: gitlab_com_derisk default_enabled: false -- GitLab From 6dccdcc7967064833c7c17844b4a306e41ae72e2 Mon Sep 17 00:00:00 2001 From: smriti Date: Wed, 17 Dec 2025 10:23:46 +0530 Subject: [PATCH 6/6] Undercoverage resolved --- .../namespaces/service_accounts/create_service_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ee/spec/services/namespaces/service_accounts/create_service_spec.rb b/ee/spec/services/namespaces/service_accounts/create_service_spec.rb index 13d9a71ed3977d..291c9eaa838567 100644 --- a/ee/spec/services/namespaces/service_accounts/create_service_spec.rb +++ b/ee/spec/services/namespaces/service_accounts/create_service_spec.rb @@ -368,6 +368,16 @@ end end + context 'when namespace does not have a gitlab_subscription' do + let_it_be(:group_without_subscription) { create(:group, owners: current_user) } + let(:namespace_id) { group_without_subscription.id } + + it 'produces an error' do + expect(result.status).to eq(:error) + expect(result.message).to include('No more seats are available to create Service Account User') + end + end + context 'when subscription has expired' do let_it_be(:group_with_expired_subscription) { create(:group, owners: current_user) } let(:namespace_id) { group_with_expired_subscription.id } -- GitLab