diff --git a/app/services/personal_access_tokens/create_service.rb b/app/services/personal_access_tokens/create_service.rb index c31caaf864e4b1e7d342984c7c10ae2d6b33c961..182470fef5514cb5b22c67bc6cbe7cbab3def4b4 100644 --- a/app/services/personal_access_tokens/create_service.rb +++ b/app/services/personal_access_tokens/create_service.rb @@ -36,7 +36,7 @@ def personal_access_token_params { name: params[:name], user_type: target_user.user_type, - group_id: group_id, + group_id: params[:group_id] || group_id, impersonation: params[:impersonation] || false, scopes: params[:scopes], expires_at: pat_expiration, diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb index 964295ea089de0852a7ba592d22b372d64d58f8d..bc808532e04d64662bd3da2c2e7b44c88676b8ee 100644 --- a/app/services/resource_access_tokens/create_service.rb +++ b/app/services/resource_access_tokens/create_service.rb @@ -105,7 +105,9 @@ def personal_access_token_params impersonation: false, scopes: params[:scopes] || default_scopes, expires_at: pat_expiration, - description: params[:description] + description: params[:description], + group_id: pat_group_id, + user_type: default_user_params[:user_type] } end @@ -133,6 +135,11 @@ def bot_namespace resource.project_namespace end + def pat_group_id + root = resource.root_ancestor + root.id if root.is_a?(::Group) + end + def log_event(token) ::Gitlab::AppLogger.info "PROJECT ACCESS TOKEN CREATION: created_by: #{current_user.username}, project_id: #{resource.id}, token_user: #{token.user.name}, token_id: #{token.id}" end diff --git a/db/docs/batched_background_migrations/backfill_group_id_and_user_type_for_resource_access_tokens.yml b/db/docs/batched_background_migrations/backfill_group_id_and_user_type_for_resource_access_tokens.yml new file mode 100644 index 0000000000000000000000000000000000000000..e519b16247151249916940fdb3e0f2f592cb377c --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_group_id_and_user_type_for_resource_access_tokens.yml @@ -0,0 +1,9 @@ +--- +migration_job_name: BackfillGroupIdAndUserTypeForResourceAccessTokens +description: Backfills `group_id` based on `user_details.bot_namespace_id` and + `user_type` based on `users.user_type` for existing group and project personal_access_tokens. +feature_category: system_access +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/203974 +milestone: '18.4' +queued_migration_version: 20250905043307 +finalized_by: # version of the migration that finalized this BBM diff --git a/db/post_migrate/20250905043307_queue_backfill_group_id_and_user_type_for_resource_access_tokens.rb b/db/post_migrate/20250905043307_queue_backfill_group_id_and_user_type_for_resource_access_tokens.rb new file mode 100644 index 0000000000000000000000000000000000000000..b80a82603ee900189527a72d81a3fb8f810c3c85 --- /dev/null +++ b/db/post_migrate/20250905043307_queue_backfill_group_id_and_user_type_for_resource_access_tokens.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class QueueBackfillGroupIdAndUserTypeForResourceAccessTokens < Gitlab::Database::Migration[2.3] + milestone '18.4' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "BackfillGroupIdAndUserTypeForResourceAccessTokens" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + def up + queue_batched_background_migration( + MIGRATION, + :personal_access_tokens, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :personal_access_tokens, :id, []) + end +end diff --git a/db/schema_migrations/20250905043307 b/db/schema_migrations/20250905043307 new file mode 100644 index 0000000000000000000000000000000000000000..297f9f95d81d3bab5c2b7225469ef48e42bbe021 --- /dev/null +++ b/db/schema_migrations/20250905043307 @@ -0,0 +1 @@ +a860e3f5b1e36c7520214746246c13eefd98e7a6605560982d25e989e725248e \ No newline at end of file diff --git a/lib/gitlab/background_migration/backfill_group_id_and_user_type_for_resource_access_tokens.rb b/lib/gitlab/background_migration/backfill_group_id_and_user_type_for_resource_access_tokens.rb new file mode 100644 index 0000000000000000000000000000000000000000..fad6ba594dc7e0db8d93eaa481be5d3035208ae3 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_group_id_and_user_type_for_resource_access_tokens.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/database/batched_background_migrations.html +# for more information on how to use batched background migrations + +# Update below commented lines with appropriate values. + +module Gitlab + module BackgroundMigration + class BackfillGroupIdAndUserTypeForResourceAccessTokens < BatchedMigrationJob + operation_name :backfill_group_id_and_user_type_for_resource_access_tokens + feature_category :system_access + + def perform + each_sub_batch do |sub_batch| + # Note: Postgresql uses 1-indexing for array access, not 0-indexing + # traversal_ids are serialized with top-level group first, and lower levels in order + connection.execute( + <<~SQL + UPDATE personal_access_tokens + SET user_type=users.user_type, group_id=(CASE + WHEN root_namespace.type = 'Group' THEN root_namespace.id + ELSE personal_access_tokens.group_id + END) + FROM + users + LEFT JOIN user_details ON user_details.user_id=users.id + LEFT JOIN namespaces bot_namespace ON bot_namespace.id=user_details.bot_namespace_id + LEFT JOIN namespaces root_namespace ON root_namespace.id=bot_namespace.traversal_ids[1] + WHERE + personal_access_tokens.id IN (#{sub_batch.select(:id).limit(sub_batch_size).to_sql}) + AND personal_access_tokens.user_type IS NULL + AND personal_access_tokens.user_id=users.id + AND users.user_type = 6 + SQL + ) + end + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_group_id_and_user_type_for_resource_access_tokens_spec.rb b/spec/lib/gitlab/background_migration/backfill_group_id_and_user_type_for_resource_access_tokens_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4b6f6cde8f8359e7cd2103671882d96608a8b286 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_group_id_and_user_type_for_resource_access_tokens_spec.rb @@ -0,0 +1,510 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# rubocop:disable RSpec/MultipleMemoizedHelpers -- We need this many for this background migration +RSpec.describe Gitlab::BackgroundMigration::BackfillGroupIdAndUserTypeForResourceAccessTokens, feature_category: :system_access do + subject(:migration) do + described_class.new( + batch_table: :personal_access_tokens, + batch_column: :id, + sub_batch_size: 100, + pause_ms: 100, + connection: ApplicationRecord.connection + ) + end + + let(:organizations) { table(:organizations) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:user_details) { table(:user_details) } + let(:personal_access_tokens) { table(:personal_access_tokens) } + + let!(:organization) { organizations.create!(name: 'organization', path: 'organization') } + + let(:group1) { namespaces.create!(name: 'group1', path: 'group1', type: 'Group', organization_id: organization.id) } + + let!(:human_user) do + users.create!( + username: 'human_user', + email: 'human_user@example.com', + user_type: 0, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:human_user_namespace) do + namespaces.create!( + name: 'HumanUserNamespace', + path: 'human_user', + type: 'User', + owner_id: human_user.id, + organization_id: organization.id + ) + end + + let!(:human_user_details) do + user_details.create!( + user_id: human_user.id + ) + end + + let!(:human_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'human_user_personal_access_token1', + user_id: human_user.id, + organization_id: human_user.organization_id + ) + end + + let!(:human_user_personal_access_token2) do + personal_access_tokens.create!( + name: 'human_user_personal_access_token2', + user_id: human_user.id, + organization_id: human_user.organization_id + ) + end + + let!(:human_user_without_user_details) do + users.create!( + username: 'human_user_without_user_details', + email: 'human_user_without_user_details@example.com', + user_type: 0, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:human_user_without_user_details_personal_access_token1) do + personal_access_tokens.create!( + name: 'human_user_without_user_details_personal_access_token1', + user_id: human_user_without_user_details.id, + organization_id: human_user_without_user_details.organization_id + ) + end + + let!(:human_user_without_user_details_personal_access_token2) do + personal_access_tokens.create!( + name: 'human_user_without_user_details_personal_access_token2', + user_id: human_user_without_user_details.id, + organization_id: human_user_without_user_details.organization_id + ) + end + + let!(:enterprise_user1) do + users.create!( + username: 'enterprise_user1', + email: 'enterprise_user1@example.com', + user_type: 0, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:enterprise_user1_details) do + user_details.create!( + user_id: enterprise_user1.id, + enterprise_group_id: group1.id + ) + end + + let!(:enterprise_user1_personal_access_token1) do + personal_access_tokens.create!( + name: 'enterprise_user1_personal_access_token1', + user_id: enterprise_user1.id, + organization_id: enterprise_user1.organization_id + ) + end + + ################################################ + # project1: Project not part of a group + ################################################ + + let!(:project1_namespace) do + namespaces.create!( + name: 'Project1', + path: 'project1', + type: 'Project', + traversal_ids: [human_user_namespace.id], + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [human_user_namespace.id, ns.id]) + end + end + + let!(:project1) do + projects.create!( + name: 'Project1', + path: 'project1', + namespace_id: human_user_namespace.id, + project_namespace_id: project1_namespace.id, + organization_id: organization.id + ) + end + + let!(:project1_bot_user) do + users.create!( + username: 'project1_bot_user', + email: 'project1_bot_user@example.com', + user_type: 6, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:project1_bot_user_details) do + user_details.create!( + user_id: project1_bot_user.id, + bot_namespace_id: project1_namespace.id + ) + end + + let!(:project1_bot_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'project_bot_user_personal_access_token1', + user_id: project1_bot_user.id, + organization_id: project1_bot_user.organization_id + ) + end + + let!(:project1_bot_user_personal_access_token2) do + personal_access_tokens.create!( + name: 'project_bot_user_personal_access_token2', + user_id: project1_bot_user.id, + organization_id: project1_bot_user.organization_id + ) + end + + ################################################ + # Service Account user + ################################################ + + let!(:service_account_user) do + users.create!( + username: 'service_account_user', + email: 'service_account_user@example.com', + user_type: 13, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:service_account_user_details) do + user_details.create!( + user_id: service_account_user.id + ) + end + + let!(:service_account_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'service_account_user_personal_access_token1', + user_id: service_account_user.id, + organization_id: service_account_user.organization_id + ) + end + + let!(:service_account_user_personal_access_token2) do + personal_access_tokens.create!( + name: 'service_account_user_personal_access_token2', + user_id: service_account_user.id, + organization_id: service_account_user.organization_id + ) + end + + ################################################ + # project2: Project part of a top-level group + ################################################ + + let!(:project2_group) do + namespaces.create!( + name: 'Project2Group', + path: 'project2group', + type: 'Group', + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [ns.id]) + end + end + + let!(:project2_namespace) do + namespaces.create!( + name: 'Project2Namespace', + path: 'project2namespace', + type: 'Project', + parent_id: project2_group.id, + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [project2_group.id, ns.id]) + end + end + + let!(:project2) do + projects.create!( + name: 'Project2', + path: 'project2', + namespace_id: project2_group.id, + project_namespace_id: project2_namespace.id, + organization_id: organization.id + ) + end + + let!(:project2_bot_user) do + users.create!( + username: 'project2_bot_user', + email: 'project2_bot_user@example.com', + user_type: 6, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:project2_bot_user_details) do + user_details.create!( + user_id: project2_bot_user.id, + bot_namespace_id: project2_namespace.id + ) + end + + let!(:project2_bot_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'project2_bot_user_personal_access_token1', + user_id: project2_bot_user.id, + organization_id: project2_bot_user.organization_id + ) + end + + ################################################ + # project3: Project part of a top-level group + ################################################ + + let!(:project3_parent_group) do + namespaces.create!( + name: 'project3ParentGroup', + path: 'project3parentgroup', + type: 'Group', + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [ns.id]) + end + end + + let!(:project3_group) do + namespaces.create!( + name: 'project3Group', + path: 'project3group', + type: 'Group', + parent_id: project3_parent_group.id, + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [project3_parent_group.id, ns.id]) + end + end + + let!(:project3_namespace) do + namespaces.create!( + name: 'project3Namespace', + path: 'project3namespace', + type: 'Project', + parent_id: project3_group.id, + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [project3_parent_group.id, project3_group.id, ns.id]) + end + end + + let!(:project3) do + projects.create!( + name: 'project3', + path: 'project3', + namespace_id: project3_group.id, + project_namespace_id: project3_namespace.id, + organization_id: organization.id + ) + end + + let!(:project3_bot_user) do + users.create!( + username: 'project3_bot_user', + email: 'project3_bot_user@example.com', + user_type: 6, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:project3_bot_user_details) do + user_details.create!( + user_id: project3_bot_user.id, + bot_namespace_id: project3_namespace.id + ) + end + + let!(:project3_bot_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'project3_bot_user_personal_access_token1', + user_id: project3_bot_user.id, + organization_id: project3_bot_user.organization_id + ) + end + + ################################################ + # group4: Group access token in sub-group + ################################################ + + let!(:group4_parent_group) do + namespaces.create!( + name: 'group4ParentGroup', + path: 'group4parentgroup', + type: 'Group', + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [ns.id]) + end + end + + let!(:group4_group) do + namespaces.create!( + name: 'group4Group', + path: 'group4group', + type: 'Group', + parent_id: group4_parent_group.id, + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [group4_parent_group.id, ns.id]) + end + end + + let!(:group4_bot_user) do + users.create!( + username: 'group4_bot_user', + email: 'group4_bot_user@example.com', + user_type: 6, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:group4_bot_user_details) do + user_details.create!( + user_id: group4_bot_user.id, + bot_namespace_id: group4_group.id + ) + end + + let!(:group4_bot_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'group4_bot_user_personal_access_token1', + user_id: group4_bot_user.id, + organization_id: group4_bot_user.organization_id + ) + end + + ################################################ + # group5: Group access token in top-level-group + ################################################ + + let!(:group5_group) do + namespaces.create!( + name: 'group5Group', + path: 'group5group', + type: 'Group', + organization_id: organization.id + ).tap do |ns| + ns.update!(traversal_ids: [ns.id]) + end + end + + let!(:group5_bot_user) do + users.create!( + username: 'group5_bot_user', + email: 'group5_bot_user@example.com', + user_type: 6, + projects_limit: 10, + organization_id: organization.id + ) + end + + let!(:group5_bot_user_details) do + user_details.create!( + user_id: group5_bot_user.id, + bot_namespace_id: group5_group.id + ) + end + + let!(:group5_bot_user_personal_access_token1) do + personal_access_tokens.create!( + name: 'group5_bot_user_personal_access_token1', + user_id: group5_bot_user.id, + organization_id: group5_bot_user.organization_id + ) + end + + ################################################ + # Begin Specs + ################################################ + + it "backfills group_id and user_type for resource access tokens", :aggregate_failures do + expect(project1_bot_user_personal_access_token1.group_id).to be_nil + expect(project1_bot_user_personal_access_token1.user_type).to be_nil + + expect(project1_bot_user_personal_access_token2.group_id).to be_nil + expect(project1_bot_user_personal_access_token2.user_type).to be_nil + + expect(project2_bot_user_personal_access_token1.group_id).to be_nil + expect(project2_bot_user_personal_access_token1.user_type).to be_nil + + expect(project3_bot_user_personal_access_token1.group_id).to be_nil + expect(project3_bot_user_personal_access_token1.user_type).to be_nil + + expect(group4_bot_user_personal_access_token1.group_id).to be_nil + expect(group4_bot_user_personal_access_token1.user_type).to be_nil + + expect(group5_bot_user_personal_access_token1.group_id).to be_nil + expect(group5_bot_user_personal_access_token1.user_type).to be_nil + + migration.perform + + expect(project1_bot_user_personal_access_token1.reload.group_id).to be_nil + expect(project1_bot_user_personal_access_token1.reload.user_type).to eq(6) + + expect(project1_bot_user_personal_access_token2.reload.group_id).to be_nil + expect(project1_bot_user_personal_access_token2.reload.user_type).to eq(6) + + expect(project2_bot_user_personal_access_token1.reload.group_id).to eq(project2_group.id) + expect(project2_bot_user_personal_access_token1.reload.user_type).to eq(6) + + expect(project3_bot_user_personal_access_token1.reload.group_id).to eq(project3_parent_group.id) + expect(project3_bot_user_personal_access_token1.reload.user_type).to eq(6) + + expect(group4_bot_user_personal_access_token1.reload.group_id).to eq(group4_parent_group.id) + expect(group4_bot_user_personal_access_token1.reload.user_type).to eq(6) + + expect(group5_bot_user_personal_access_token1.reload.group_id).to eq(group5_group.id) + expect(group5_bot_user_personal_access_token1.reload.user_type).to eq(6) + end + + it "does not backfill human users' personal access tokens", :aggregate_failures do + expect(human_user_personal_access_token1.group_id).to be_nil + expect(human_user_personal_access_token1.user_type).to be_nil + + expect(human_user_personal_access_token2.group_id).to be_nil + expect(human_user_personal_access_token2.user_type).to be_nil + + expect(enterprise_user1_personal_access_token1.group_id).to be_nil + expect(enterprise_user1_personal_access_token1.user_type).to be_nil + + migration.perform + + expect(human_user_personal_access_token1.reload.group_id).to be_nil + expect(human_user_personal_access_token1.reload.user_type).to be_nil + + expect(human_user_personal_access_token2.reload.group_id).to be_nil + expect(human_user_personal_access_token2.reload.user_type).to be_nil + + expect(enterprise_user1_personal_access_token1.reload.group_id).to be_nil + expect(enterprise_user1_personal_access_token1.reload.user_type).to be_nil + end +end +# rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/spec/migrations/20250905043307_queue_backfill_group_id_and_user_type_for_resource_access_tokens_spec.rb b/spec/migrations/20250905043307_queue_backfill_group_id_and_user_type_for_resource_access_tokens_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee3a235ecef559dc1317382fc7498e7cf09638a8 --- /dev/null +++ b/spec/migrations/20250905043307_queue_backfill_group_id_and_user_type_for_resource_access_tokens_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillGroupIdAndUserTypeForResourceAccessTokens, migration: :gitlab_main, feature_category: :system_access do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + gitlab_schema: :gitlab_main, + table_name: :personal_access_tokens, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/services/personal_access_tokens/create_service_spec.rb b/spec/services/personal_access_tokens/create_service_spec.rb index f2caf3bfbe408c30a14fd3754536c6347facf3cb..b287746de3ffea0ee1290a359efa8fe23360706c 100644 --- a/spec/services/personal_access_tokens/create_service_spec.rb +++ b/spec/services/personal_access_tokens/create_service_spec.rb @@ -73,6 +73,14 @@ end end + context 'when group_id parameter is passed' do + let(:group) { create(:group, owners: [user]) } + let(:params) { { name: 'Test token', impersonation: false, scopes: [:api], expires_at: Date.today + 1.month, description: "Test Description", group_id: group.id } } + let(:service) { described_class.new(current_user: user, organization_id: organization.id, target_user: user, params: params) } + + it { expect(subject.payload[:personal_access_token].group_id).to eq(group.id) } + end + context 'with no expires_at set', :freeze_time do let(:params) { { name: 'Test token', impersonation: false, scopes: [:no_valid] } } let(:service) { described_class.new(current_user: user, organization_id: organization.id, target_user: user, params: params) } diff --git a/spec/services/personal_access_tokens/rotate_service_spec.rb b/spec/services/personal_access_tokens/rotate_service_spec.rb index 61a9d927dd3829539b90a67ba7b8c5c5a174f940..c2d55415973967e007343faee13fe97583f0dca9 100644 --- a/spec/services/personal_access_tokens/rotate_service_spec.rb +++ b/spec/services/personal_access_tokens/rotate_service_spec.rb @@ -156,5 +156,22 @@ end end end + + context "for group resource access token" do + let_it_be(:token, reload: true) do + create(:resource_access_token, expires_at: Time.zone.today + 30.days) + end + + it_behaves_like "rotates token successfully" + end + + context "for project resource access token" do + let_it_be(:project) { create(:project, owners: [current_user]) } + let_it_be(:token, reload: true) do + create(:resource_access_token, resource: project, expires_at: Time.zone.today + 30.days) + end + + it_behaves_like "rotates token successfully" + end end end diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb index 5ad28cdcbc5d36cd19a805b4400dfa4d100080a4..27e12d3af4ca79988335355997a5957dfb109364 100644 --- a/spec/services/resource_access_tokens/create_service_spec.rb +++ b/spec/services/resource_access_tokens/create_service_spec.rb @@ -60,6 +60,8 @@ response = subject access_token = response.payload[:access_token] namespace = resource.is_a?(Group) ? resource : resource.project_namespace + root = resource.root_ancestor + group_id = root.is_a?(Group) ? root.id : nil access_token.user.reload expect(access_token.user.confirmed?).to eq(true) @@ -68,6 +70,8 @@ expect(access_token.user.namespace.organization.id).to eq(resource.organization.id) expect(access_token.organization.id).to eq(resource.organization.id) expect(access_token.user.bot_namespace).to eq(namespace) + expect(access_token.user_type).to eq("project_bot") + expect(access_token.group_id).to eq(group_id) end end @@ -399,5 +403,40 @@ end end end + + context 'when resource is a sub-group' do + let_it_be(:resource_type) { 'group' } + let_it_be(:parent_group) { create(:group, :private, organization: organization) } + let_it_be(:child_group) { create(:group, :private, organization: organization, parent: parent_group) } + let_it_be(:resource) { child_group } + + it_behaves_like 'when user does not have permission to create a resource bot' + + context 'user with valid permission' do + before_all do + resource.add_owner(user) + end + + it_behaves_like 'allows creation of bot with valid params' + end + end + + context 'when resource is a project inside a sub-group' do + let_it_be(:resource_type) { 'project' } + let_it_be(:parent_group) { create(:group, :private, organization: organization) } + let_it_be(:child_group) { create(:group, :private, organization: organization, parent: parent_group) } + let_it_be(:child_project) { create(:project, :private, organization: organization, namespace: child_group) } + let_it_be(:resource) { child_project } + + it_behaves_like 'when user does not have permission to create a resource bot' + + context 'user with valid permission' do + before_all do + resource.add_owner(user) + end + + it_behaves_like 'allows creation of bot with valid params' + end + end end end