diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 282acb448efc8d985a118094d09296915a9958c5..14219b24b5d6af81c188eb41ed20b6e6a75f3a39 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -81,6 +81,14 @@ production: &base allowed_hosts: [] + # ActionCable allowed request origins + # Customize if you browse your GitLab application through multiple URLs + # If you have GitLab Geo enabled, then add the external URLs of every site: + action_cable_allowed_origins: + #- https://unified.url + #- https://primary-external.url (if different from the above) + #- https://secondary-external.url (if different from the above) + # Trusted Proxies # Customize if you have GitLab behind a reverse proxy which is running on a different machine. # Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index fd3d4fa5cc06c5a83f5882b2f888b5aa6bd68fee..6c2562dd9d4a60d447bf4d3ca25bf9fe038636f9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -275,6 +275,11 @@ Settings.zoekt['bin_path'] ||= Gitlab::Utils.which('gitlab-zoekt') end +# +# ActionCable +# +Settings.gitlab['action_cable_allowed_origins'] ||= [] + # # CI # diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb index b2ac3e8c1ae0dcdb9104294d82f9374dcaefb85e..ed45559909a72cac36aa930c4e45761a489cb09f 100644 --- a/config/initializers/action_cable.rb +++ b/config/initializers/action_cable.rb @@ -7,12 +7,31 @@ config.action_cable.url = Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/cable') config.action_cable.worker_pool_size = Gitlab::ActionCable::Config.worker_pool_size - config.action_cable.allowed_request_origins = [Gitlab.config.gitlab.url] if Rails.env.development? || Rails.env.test? if Rails.env.development? || Rails.env.test? + config.action_cable.allowed_request_origins = [Gitlab.config.gitlab.url] config.action_cable.disable_request_forgery_protection = Gitlab::Utils.to_boolean( ENV.fetch('ACTION_CABLE_DISABLE_REQUEST_FORGERY_PROTECTION', false) ) end + + if Gitlab.config.gitlab.action_cable_allowed_origins.present? + # sanitize URLs + error_message = 'Invalid URL found in action_cable_allowed_origins configuration. ' \ + 'Please fix this in your gitlab.yml before starting GitLab.' + + allowed_origins = Gitlab.config.gitlab.action_cable_allowed_origins.filter_map do |url| + begin + uri = URI.parse(url) + raise error_message unless uri.is_a?(URI::HTTP) && uri.host.present? + rescue URI::InvalidURIError + raise error_message + end + + url.sub(%r{/+$}, '') + end + + config.action_cable.allowed_request_origins = allowed_origins if allowed_origins.present? + end end ActionCable::SubscriptionAdapter::Base.prepend(Gitlab::Patch::ActionCableSubscriptionAdapterIdentifier) diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md index bf7fc3c13a80b9c52b87392dd596264fe7cdb6f7..df90bca7ba39920660740664dd8da51a22320d8e 100644 --- a/doc/administration/geo/replication/configuration.md +++ b/doc/administration/geo/replication/configuration.md @@ -281,6 +281,30 @@ that the **secondary** site can act on those notifications immediately. Be sure the secondary site is running and accessible. You can sign in to the secondary site with the same credentials as were used with the primary site. +### Add primary and secondary URLs as allowed ActionCable origins + +This step allows websockets to work seamlessly from primary and secondary sites. + +1. Collect the **external URLs** of your sites (primary and secondary). You can find them in the Site pages in the Admin area, as mentioned in the section above. +1. SSH into each Rails and Sidekiq node on your **primary site** and sign in as root: + + ```shell + sudo -i + ``` + +1. Edit `/etc/gitlab/gitlab.rb` to add the URLs collected in step 1 to the `action_cable_allowed_origins` setting: + + ```ruby + gitlab_rails['action_cable_allowed_origins'] = ['https://secondary.example.com', 'https://primary.example.com'] + ``` + +1. To apply the changes, reconfigure each Rails and Sidekiq node and restart the service: + + ```shell + gitlab-ctl reconfigure + gitlab-ctl restart + ``` + ## Step 4. (Optional) Using custom certificates You can safely skip this step if: diff --git a/doc/administration/geo/setup/two_single_node_sites.md b/doc/administration/geo/setup/two_single_node_sites.md index 6136c71af78dbb9e61d4522f4138a0da35cb670b..f2677d8be343f40b5bec00a7aafa759cb629827d 100644 --- a/doc/administration/geo/setup/two_single_node_sites.md +++ b/doc/administration/geo/setup/two_single_node_sites.md @@ -676,6 +676,30 @@ that the secondary site can act on the notifications immediately. Be sure the secondary site is running and accessible. You can sign in to the secondary site with the same credentials as were used with the primary site. +### Add primary and secondary URLs as allowed ActionCable origins + +This step allows websockets to work seamlessly from primary and secondary sites. + +1. Collect the **external URLs** of your sites (primary and secondary). You can find them in the Site pages in the Admin area, as mentioned in the section above. +1. SSH into each Rails and Sidekiq node on your **primary site** and sign in as root: + + ```shell + sudo -i + ``` + +1. Edit `/etc/gitlab/gitlab.rb` to add the URLs collected in step 1 to the `action_cable_allowed_origins` setting: + + ```ruby + gitlab_rails['action_cable_allowed_origins'] = ['https://secondary.example.com', 'https://primary.example.com'] + ``` + +1. To apply the changes, reconfigure each Rails and Sidekiq node and restart the service: + + ```shell + gitlab-ctl reconfigure + gitlab-ctl restart + ``` + ### Enable Git access over HTTP/HTTPS and SSH Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone @@ -765,6 +789,9 @@ gitlab_rails['monitoring_whitelist'] = ['127.0.0.0/8', '10.0.0.0/8'] gitaly['configuration'] = { prometheus_listen_addr: '0.0.0.0:9236', } + +## ActionCable allowed origins +gitlab_rails['action_cable_allowed_origins'] = ['https://secondary.example.com', 'https://primary.example.com'] ``` ### Complete secondary site diff --git a/ee/spec/initializers/1_settings_spec.rb b/ee/spec/initializers/1_settings_spec.rb index 41df16c5ab142c61a9010c1dfb9944c6d3ffbd2c..a2d5e0ae39a0df5e0d8ccd0791622915868561df 100644 --- a/ee/spec/initializers/1_settings_spec.rb +++ b/ee/spec/initializers/1_settings_spec.rb @@ -445,4 +445,64 @@ end end end + + describe 'ActionCable allowed origins' do + let(:config) { {} } + + before do + Settings.gitlab = config + load_settings + end + + after do + Settings.gitlab = {} + load_settings + end + + it 'returns default setting' do + expect(Settings.gitlab.action_cable_allowed_origins).to eq([]) + end + + context 'with settings' do + let(:config) { { action_cable_allowed_origins: %w[http://origin1.url http://origin2.url] } } + + it 'uses provided config' do + expect(Settings.gitlab.action_cable_allowed_origins).to eq(%w[http://origin1.url http://origin2.url]) + end + end + end + + describe 'geo' do + let(:config) { {} } + + before do + Settings.geo = config + load_settings + end + + after do + Settings.geo = {} + load_settings + end + + it 'provides default config' do + expect(Settings.geo.node_name).to eq(Settings.gitlab['url']) + expect(Settings.geo.registry_replication['enabled']).to eq(false) + end + + context 'when config is provided' do + let(:config) do + { + node_name: 'my primary node', + registry_replication: { enabled: true, primary_api_url: 'http://primary.url' } + } + end + + it 'uses provided config' do + expect(Settings.geo.node_name).to eq('my primary node') + expect(Settings.geo.registry_replication['enabled']).to eq(true) + expect(Settings.geo.registry_replication['primary_api_url']).to eq('http://primary.url') + end + end + end end diff --git a/spec/initializers/action_cable_spec.rb b/spec/initializers/action_cable_spec.rb index 0cdac970c4adfe57b120ab1fa2b1e6b625200d35..7b284c281366652c53ef574dd5f50f7438a14078 100644 --- a/spec/initializers/action_cable_spec.rb +++ b/spec/initializers/action_cable_spec.rb @@ -130,4 +130,91 @@ end end end + + describe 'config.allowed_request_origins setting' do + before do + stub_config_setting(action_cable_allowed_origins: origins) + stub_rails_env(rails_env) if rails_env + end + + around do |example| + old = config.deep_dup + Rails.application.config.action_cable.clear + example.run + ensure + Rails.application.config.action_cable = old + end + + let(:load_config) { load Rails.root.join('config/initializers/action_cable.rb') } + let(:config) { Rails.application.config.action_cable } + let(:rails_env) { nil } + let_it_be(:message) do + 'Invalid URL found in action_cable_allowed_origins configuration. ' \ + 'Please fix this in your gitlab.yml before starting GitLab.' + end + + context 'with valid and invalid origins' do + let(:origins) { ['http://test.com/', 'invalid_url'] } + + it 'raises an exception' do + expect { load_config }.to raise_error(RuntimeError, message) + end + end + + context 'with invalid origins' do + let(:origins) { ['invalid_url'] } + + it 'raises an exception' do + expect { load_config }.to raise_error(RuntimeError, message) + end + end + + context 'with default setting' do + let(:origins) { [] } + + before do + load_config + end + + it 'returns localhost' do + expect(config.allowed_request_origins).to eq(["http://localhost"]) + end + + context 'when in production' do + let(:rails_env) { 'production' } + + it 'returns nil' do + expect(config.allowed_request_origins).to be_nil + end + end + end + + context 'with valid origins' do + shared_examples 'returns the passed value with no ending slash' do + it 'returns the passed values without ending slash' do + load_config + + expect(config.allowed_request_origins).to contain_exactly('http://test.com') + end + end + + context 'when origin contains no trailing slash' do + let(:origins) { ['http://test.com'] } + + it_behaves_like 'returns the passed value with no ending slash' + end + + context 'when origin contains one trailing slash' do + let(:origins) { ['http://test.com/'] } + + it_behaves_like 'returns the passed value with no ending slash' + end + + context 'when origin contains several trailing slashes' do + let(:origins) { ['http://test.com//'] } + + it_behaves_like 'returns the passed value with no ending slash' + end + end + end end