diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000000000000000000000000000000000000..b442b3576fb0a22dbcae42eaebfafd2a67b4f80f --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Current < ActiveSupport::CurrentAttributes # rubocop:disable Gitlab/NamespacedClass -- We want this to be top level due to scope of use and no namespace due to ease of calling + # watch background jobs need to reset on each job if using + attribute :organization +end diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index d300f2f225208c10d30fed20f9349eb331e50014..0875b57e79b251e6b764d02661a1df21ebae4ead 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -39,12 +39,16 @@ def self.default_organization find_by(id: DEFAULT_ORGANIZATION_ID) end + def self.default?(id) + id == DEFAULT_ORGANIZATION_ID + end + def organization_detail super.presence || build_organization_detail end def default? - id == DEFAULT_ORGANIZATION_ID + self.class.default?(id) end def to_param diff --git a/app/models/organizations/organization_setting.rb b/app/models/organizations/organization_setting.rb index fb141c164d30fe32d27973882bf4adc7a70e9601..7ddf2eb62785a3fee574311afec0ec2d2cfa73a9 100644 --- a/app/models/organizations/organization_setting.rb +++ b/app/models/organizations/organization_setting.rb @@ -2,8 +2,6 @@ module Organizations class OrganizationSetting < ApplicationRecord - extend ::Organization::CurrentOrganization - belongs_to :organization validates :settings, json_schema: { filename: "organization_settings" } @@ -20,9 +18,9 @@ class OrganizationSetting < ApplicationRecord end def self.for_current_organization - return unless current_organization + return unless Current.organization - current_organization.settings || current_organization.build_settings + Current.organization.settings || Current.organization.build_settings end end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 0fdc73ef2b3b3e0469a8b081bce1cfafd4e16cf4..b99fcad785c55cecf0cfb7c491e948f8b434ad95 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -2,8 +2,6 @@ module Groups class CreateService < Groups::BaseService - include Organization::CurrentOrganization - def initialize(user, params = {}) @current_user = user @params = params.dup @@ -17,7 +15,7 @@ def execute @group = Group.new(params.except(*::NamespaceSetting.allowed_namespace_settings_params)) - set_organization unless @params[:organization_id] + set_organization @group.build_namespace_settings handle_namespace_settings @@ -136,7 +134,11 @@ def organization_setting_valid? # 2. We shouldn't need to check if this is allowed if the user didn't try to set it themselves. i.e. # provided in the params return true if params[:organization_id].blank? - return true if @group.organization.blank? + # There is a chance the organization is still blank(if not default organization), but that is the only case + # where we should allow this to not actually be a record in the database. + # Otherwise it isn't valid to set this to a non-existent record id and we'll check that in the lines after + # this code. + return true if @group.organization.blank? && Organizations::Organization.default?(params[:organization_id]) can_create_group_in_organization? && matches_parent_organization? end @@ -164,10 +166,16 @@ def inherit_group_shared_runners_settings end def set_organization - if @group.parent_id + if params[:organization_id] + nil # nothing to do, already assigned from params + elsif @group.parent_id @group.organization = @group.parent.organization - elsif current_organization - @group.organization = current_organization + # Rely on middleware setting of the organization, + # but since we are guarding with feature flag, we need to check it first + # Consider replacing elsif with else in cleanup of current_organization_middleware feature flag in + # https://gitlab.com/gitlab-org/gitlab/-/issues/441121 + elsif Current.organization + @group.organization = Current.organization end end end diff --git a/config/feature_flags/gitlab_com_derisk/current_organization_middleware.yml b/config/feature_flags/gitlab_com_derisk/current_organization_middleware.yml new file mode 100644 index 0000000000000000000000000000000000000000..fc391f89762830fbb91dd94c86f20401be641d19 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/current_organization_middleware.yml @@ -0,0 +1,9 @@ +--- +name: current_organization_middleware +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437541 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/143978 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/441121 +milestone: '16.10' +group: group::tenant scale +type: gitlab_com_derisk +default_enabled: false diff --git a/config/initializers/current_organization.rb b/config/initializers/current_organization.rb new file mode 100644 index 0000000000000000000000000000000000000000..9c9e9ac09d14d64ce4f40a63a65b3b15ac6989f3 --- /dev/null +++ b/config/initializers/current_organization.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Rails.application.configure do |config| + config.middleware.use(Gitlab::Middleware::Organizations::Current) +end diff --git a/ee/spec/requests/api/graphql/vulnerabilities/issue_links_spec.rb b/ee/spec/requests/api/graphql/vulnerabilities/issue_links_spec.rb index 8ce9216f2ac50727516732696ece8309bfe06223..fcef65b569e536a9572c6dc64ecd24976e17175e 100644 --- a/ee/spec/requests/api/graphql/vulnerabilities/issue_links_spec.rb +++ b/ee/spec/requests/api/graphql/vulnerabilities/issue_links_spec.rb @@ -103,7 +103,8 @@ # 26) Loading the project authorizations # 27) Loading the namespace # 28) Loading the user - expect { query_issue_links }.not_to exceed_query_limit(28) + # 29) Loading the organization + expect { query_issue_links }.not_to exceed_query_limit(29) end end diff --git a/lib/gitlab/middleware/organizations/current.rb b/lib/gitlab/middleware/organizations/current.rb new file mode 100644 index 0000000000000000000000000000000000000000..5baf75058615c4ec298c5a4682a63a4105540efc --- /dev/null +++ b/lib/gitlab/middleware/organizations/current.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Middleware + module Organizations + class Current + def initialize(app) + @app = app + end + + def call(env) + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/437541 to enhance the finder: + # - Separate logged in vs not logged in user(perhaps using session) + # - Authenticated: + # - Request header + # - Rails session value to drive the finder + # - First organization current user is a user of + # - Unauthenticated: + # - default organization + if Feature.enabled?(:current_organization_middleware, type: :gitlab_com_derisk) + ::Current.organization = ::Organizations::Organization.default_organization + end + + @app.call(env) + end + end + end + end +end diff --git a/lib/organization/current_organization.rb b/lib/organization/current_organization.rb deleted file mode 100644 index d1f50aba7a1825a452bcd3739c1055cb82bf931e..0000000000000000000000000000000000000000 --- a/lib/organization/current_organization.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Organization - module CurrentOrganization - CURRENT_ORGANIZATION_THREAD_VAR = :current_organization - - def current_organization - Thread.current[CURRENT_ORGANIZATION_THREAD_VAR] - end - - def current_organization=(organization) - Thread.current[CURRENT_ORGANIZATION_THREAD_VAR] = organization - end - - def with_current_organization(organization, &_blk) - previous_organization = current_organization - self.current_organization = organization - yield - ensure - self.current_organization = previous_organization - end - end -end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index aac2e3c4e46870f0eb2860ca724bb5eafbfd357a..1c6394d3e56196901ed36d87b2b5fafb568ff574 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -35,6 +35,20 @@ expect(page).to have_current_path(group_path(group), ignore_query: true) expect(page).to have_selector '.visibility-icon [data-testid="earth-icon"]' end + + context 'with current organization setting in middleware' do + it 'sets the organization to the default organization' do + default_organization = create(:organization, :default) + + fill_in 'Group name', with: 'test-group' + click_button 'Create group' + + group = Group.find_by(name: 'test-group') + + expect(group.organization).to eq(default_organization) + expect(page).to have_current_path(group_path(group), ignore_query: true) + end + end end describe 'with expected fields' do diff --git a/spec/lib/gitlab/middleware/organizations/current_spec.rb b/spec/lib/gitlab/middleware/organizations/current_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2c84c2975b0c4b101d0e46da7c6be5e7bae6edd0 --- /dev/null +++ b/spec/lib/gitlab/middleware/organizations/current_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Middleware::Organizations::Current, feature_category: :cell do + subject(:perform_request) do + path = '/' + app = ->(env) { [200, env, 'app'] } + middleware = described_class.new(app) + Rack::MockRequest.new(middleware).get(path) + end + + context 'with an existing default organization' do + let_it_be(:organization) { create(:organization, :default) } + + before_all do + create(:organization) # prove we are really being selective for the default org + end + + after do + Current.reset + end + + it 'loads the current organization' do + perform_request + + expect(Current.organization).to eq(organization) + end + + context 'when current_organization_middleware feature flag is disabled' do + before do + stub_feature_flags(current_organization_middleware: false) + end + + it 'does not set the organization' do + perform_request + + expect(Current.organization).to eq(nil) + end + end + end + + context 'without an existing default organization' do + it 'sets the current organization to nil' do + perform_request + + expect(Current.organization).to eq(nil) + end + end +end diff --git a/spec/lib/organization/current_organization_spec.rb b/spec/lib/organization/current_organization_spec.rb deleted file mode 100644 index ffd37ac4de9eea71cf72f8fc8e6d6f40963f0745..0000000000000000000000000000000000000000 --- a/spec/lib/organization/current_organization_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Organization::CurrentOrganization, feature_category: :organization do - include described_class - - after do - # Wipe thread variables between specs. - Thread.current[described_class::CURRENT_ORGANIZATION_THREAD_VAR] = nil - end - - describe '.current_organization' do - subject { current_organization } - - context 'when current organization is set' do - let(:some_organization) { create(:organization) } - - before do - self.current_organization = some_organization - end - - it { is_expected.to eq some_organization } - end - - context 'when organization is not set' do - it { is_expected.to be_nil } - end - end - - describe '.current_organization=' do - subject(:setter) { self.current_organization = some_organization } - - let(:some_organization) { create(:organization) } - - it 'sets current organization' do - expect { setter }.to change { current_organization }.from(nil).to(some_organization) - end - end - - describe '.with_current_organization' do - let(:some_organization) { create(:organization) } - - it 'sets current organization within block' do - expect(current_organization).to be_nil - with_current_organization(some_organization) do - expect(current_organization).to eq some_organization - end - expect(current_organization).to be_nil - end - - context 'when an error is raised' do - it 'resets current organization' do - begin - with_current_organization(some_organization) do - raise StandardError - end - rescue StandardError - nil - end - - expect(current_organization).to be_nil - end - end - end -end diff --git a/spec/models/organizations/organization_setting_spec.rb b/spec/models/organizations/organization_setting_spec.rb index dd90a4d0f58f40b4fbb9ae9597cce30f90ce1f30..b38db1f9a650e896ff26fc90d75c3f44e7716cca 100644 --- a/spec/models/organizations/organization_setting_spec.rb +++ b/spec/models/organizations/organization_setting_spec.rb @@ -57,36 +57,26 @@ end describe '.for_current_organization' do - let(:dummy_class) { Class.new { extend Organization::CurrentOrganization } } - subject(:settings) { described_class.for_current_organization } context 'when there is no current organization' do it { is_expected.to be_nil } end - context 'when there is a current organization' do - before do - dummy_class.current_organization = organization - end - - after do - dummy_class.current_organization = nil - end + context 'when there is a current organization', :with_current_organization do + context 'when current organization has settings' do + let_it_be(:organization_setting) { create(:organization_setting, organization: current_organization) } - it 'returns current organization' do - expect(settings).to eq(organization_setting) + it 'returns current organization' do + expect(settings).to eq(organization_setting) + end end context 'when current organization does not have settings' do - before do - allow(organization).to receive(:settings).and_return(nil) - end - it 'returns new settings record' do new_settings = settings - expect(new_settings.organization).to eq(organization) + expect(new_settings.organization).to eq(current_organization) expect(new_settings.new_record?).to eq(true) end end diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb index 4fabf6ec4dcfcd2369ae5c548a3a62962fc64328..d09403512cc38fbfe6b3b3aa3b33ac764e7dfb77 100644 --- a/spec/models/organizations/organization_spec.rb +++ b/spec/models/organizations/organization_spec.rb @@ -88,6 +88,20 @@ end end + describe '.default?' do + context 'when organization is default' do + it 'returns true' do + expect(described_class.default?(default_organization.id)).to eq(true) + end + end + + context 'when organization is not default' do + it 'returns false' do + expect(described_class.default?(organization.id)).to eq(false) + end + end + end + describe '#id' do context 'when organization is default' do it 'has id 1' do diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index 1bd239ecd87c97717cb270e150e5460c875b8e12..bbac48bccb28d80b28c0d20971385365e581e83a 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -127,7 +127,7 @@ def run_mutation! context 'when passing append as true' do let(:mode) { Types::MutationOperationModeEnum.enum[:append] } let(:input) { { assignee_usernames: [assignee2.username], operation_mode: mode } } - let(:db_query_limit) { 25 } + let(:db_query_limit) { 26 } before do # In CE, APPEND is a NOOP as you can't have multiple assignees @@ -147,7 +147,7 @@ def run_mutation! end context 'when passing remove as true' do - let(:db_query_limit) { 33 } + let(:db_query_limit) { 34 } let(:mode) { Types::MutationOperationModeEnum.enum[:remove] } let(:input) { { assignee_usernames: [assignee.username], operation_mode: mode } } let(:expected_result) { [] } diff --git a/spec/requests/user_avatar_spec.rb b/spec/requests/user_avatar_spec.rb index 0a9f3784833e400bb452ec28885fc1341952ec60..f84f59a63fb9057fc2965bea9a8e82bf71df6d92 100644 --- a/spec/requests/user_avatar_spec.rb +++ b/spec/requests/user_avatar_spec.rb @@ -20,7 +20,7 @@ get user.avatar_url # Skip queries on first application load expect(response).to have_gitlab_http_status(:ok) - expect { get user.avatar_url }.not_to exceed_query_limit(3) + expect { get user.avatar_url }.not_to exceed_query_limit(4) end end @@ -30,7 +30,7 @@ get user.avatar_url # Skip queries on first application load expect(response).to have_gitlab_http_status(:ok) - expect { get user.avatar_url }.not_to exceed_query_limit(2) + expect { get user.avatar_url }.not_to exceed_query_limit(3) end end end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index a4227b3b28bcd185c68871dfc85216a3f6ae5498..0a65ed4ab165cc0797a7ec46908eaa4c13f48b1e 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -153,6 +153,18 @@ it_behaves_like 'creating a group' end + context 'when organization_id is not a valid id' do + let(:extra_params) { { organization_id: non_existing_record_id } } + + it_behaves_like 'does not create a group' + + it 'returns an error and does not set organization_id', :aggregate_failures do + expect(created_group.errors[:organization_id].first) + .to eq(s_("CreateGroup|You don't have permission to create a group in the provided organization.")) + expect(created_group.organization_id).to be_nil + end + end + context 'when user is an admin', :enable_admin_mode do let(:current_user) { create(:admin) } @@ -189,9 +201,8 @@ end end - context 'when organization is not set by params' do + context 'when organization is not set by params', :with_current_organization do let_it_be(:default_organization) { create(:organization, :default) } - let_it_be(:current_organization) { create(:organization, name: 'Current Organization') } context 'and the parent of the group has an organization' do let_it_be(:parent_group) { create(:group, organization: other_organization) } @@ -203,22 +214,18 @@ end end - context 'and current_organization is known' do - before do - allow_next_instance_of(Groups::CreateService) do |instance| - allow(instance).to receive(:current_organization).and_return(current_organization) - end - end - + context 'and has no parent group' do it 'creates group with the current organization' do expect(created_group.organization).to eq(current_organization) end end + end - context 'and no group can be found' do - it 'creates group with the default organization' do - expect(created_group.organization).to eq(default_organization) - end + context 'when organization is not set at all' do + it 'creates group without an organization' do + expect(created_group.organization).to eq(nil) + # let db default happen even if the organization record itself doesn't exist + expect(created_group.organization_id).not_to be_nil end end end diff --git a/spec/support/shared_contexts/current_organization_context.rb b/spec/support/shared_contexts/current_organization_context.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9d4685a4ab6a69878ce5e2b8c57cf4ee08366bf --- /dev/null +++ b/spec/support/shared_contexts/current_organization_context.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# goal of this context: provide an easy process for setting and using the current organization that is set +# in the middleware for non-feature spec level specs. +RSpec.shared_context 'with current_organization setting', shared_context: :metadata do # rubocop:disable RSpec/SharedGroupsMetadata -- We are actually using this for easy metadata setting + let_it_be(:current_organization, reload: true) { create(:organization, name: 'Current Organization') } + + around do |example| + Current.set(organization: current_organization) { example.run } + end +end + +RSpec.configure do |rspec| + rspec.include_context 'with current_organization setting', with_current_organization: true +end