Commit f70aea90 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '4193-ip-whitelisting-for-geo-enabling-functionality-in-the-primary' into 'master'

Resolve "IP whitelisting for Geo-enabling functionality in the primary"

Closes #4193

See merge request gitlab-org/gitlab-ee!10383
parents c2d438ec b0dc5e97
...@@ -218,6 +218,7 @@ ActiveRecord::Schema.define(version: 20190403161806) do ...@@ -218,6 +218,7 @@ ActiveRecord::Schema.define(version: 20190403161806) do
t.integer "first_day_of_week", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false
t.boolean "elasticsearch_limit_indexing", default: false, null: false t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.integer "default_project_creation", default: 2, null: false t.integer "default_project_creation", default: 2, null: false
t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0"
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
......
...@@ -64,7 +64,8 @@ Example response: ...@@ -64,7 +64,8 @@ Example response:
"instance_statistics_visibility_private": false, "instance_statistics_visibility_private": false,
"user_show_add_ssh_key_message": true, "user_show_add_ssh_key_message": true,
"file_template_project_id": 1, "file_template_project_id": 1,
"local_markdown_version": 0 "local_markdown_version": 0,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0"
} }
``` ```
...@@ -127,7 +128,8 @@ Example response: ...@@ -127,7 +128,8 @@ Example response:
"instance_statistics_visibility_private": false, "instance_statistics_visibility_private": false,
"user_show_add_ssh_key_message": true, "user_show_add_ssh_key_message": true,
"file_template_project_id": 1, "file_template_project_id": 1,
"local_markdown_version": 0 "local_markdown_version": 0,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0"
} }
``` ```
...@@ -278,3 +280,4 @@ are listed in the descriptions of the relevant settings. ...@@ -278,3 +280,4 @@ are listed in the descriptions of the relevant settings.
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | | `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
| `geo_node_allowed_ips` | string | yes | **(Premium)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
...@@ -47,15 +47,16 @@ module EE ...@@ -47,15 +47,16 @@ module EE
override :authenticate_user override :authenticate_user
def authenticate_user def authenticate_user
return super unless geo_request? return super unless geo_request?
return render_bad_geo_auth('Bad token') unless decoded_authorization return render_bad_geo_response('Request from this IP is not allowed') unless ip_allowed?
return render_bad_geo_auth('Unauthorized scope') unless jwt_scope_valid? return render_bad_geo_jwt('Bad token') unless decoded_authorization
return render_bad_geo_jwt('Unauthorized scope') unless jwt_scope_valid?
# grant access # grant access
@authentication_result = ::Gitlab::Auth::Result.new(nil, project, :geo, [:download_code, :push_code]) # rubocop:disable Gitlab/ModuleWithInstanceVariables @authentication_result = ::Gitlab::Auth::Result.new(nil, project, :geo, [:download_code, :push_code]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
rescue ::Gitlab::Geo::InvalidDecryptionKeyError rescue ::Gitlab::Geo::InvalidDecryptionKeyError
render_bad_geo_auth("Invalid decryption key") render_bad_geo_jwt("Invalid decryption key")
rescue ::Gitlab::Geo::InvalidSignatureTimeError rescue ::Gitlab::Geo::InvalidSignatureTimeError
render_bad_geo_auth("Invalid signature time ") render_bad_geo_jwt("Invalid signature time ")
end end
def jwt_scope_valid? def jwt_scope_valid?
...@@ -72,8 +73,16 @@ module EE ...@@ -72,8 +73,16 @@ module EE
end end
end end
def render_bad_geo_auth(message) def render_bad_geo_jwt(message)
render plain: "Geo JWT authentication failed: #{message}", status: :unauthorized render_bad_geo_response("Geo JWT authentication failed: #{message}")
end
def render_bad_geo_response(message)
render plain: message, status: :unauthorized
end
def ip_allowed?
::Gitlab::Geo.allowed_ip?(request.ip)
end end
end end
end end
......
...@@ -66,6 +66,7 @@ module EE ...@@ -66,6 +66,7 @@ module EE
:elasticsearch_namespace_ids, :elasticsearch_namespace_ids,
:elasticsearch_project_ids, :elasticsearch_project_ids,
:geo_status_timeout, :geo_status_timeout,
:geo_node_allowed_ips,
:help_text, :help_text,
:pseudonymizer_enabled, :pseudonymizer_enabled,
:repository_size_limit, :repository_size_limit,
......
...@@ -76,6 +76,10 @@ module EE ...@@ -76,6 +76,10 @@ module EE
presence: true, presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? } if: -> (setting) { setting.external_auth_client_cert.present? }
validates :geo_node_allowed_ips, length: { maximum: 255 }, presence: true
validate :check_geo_node_allowed_ips
validates_with X509CertificateCredentialsValidator, validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert, certificate: :external_auth_client_cert,
pkey: :external_auth_client_key, pkey: :external_auth_client_key,
...@@ -119,7 +123,8 @@ module EE ...@@ -119,7 +123,8 @@ module EE
snowplow_cookie_domain: nil, snowplow_cookie_domain: nil,
snowplow_enabled: false, snowplow_enabled: false,
snowplow_site_id: nil, snowplow_site_id: nil,
custom_project_templates_group_id: nil custom_project_templates_group_id: nil,
geo_node_allowed_ips: '0.0.0.0/0, ::/0'
) )
end end
end end
...@@ -290,5 +295,11 @@ module EE ...@@ -290,5 +295,11 @@ module EE
def email_additional_text_column_exists? def email_additional_text_column_exists?
::Gitlab::Database.cached_column_exists?(:application_settings, :email_additional_text) ::Gitlab::Database.cached_column_exists?(:application_settings, :email_additional_text)
end end
def check_geo_node_allowed_ips
::Gitlab::CIDR.new(geo_node_allowed_ips)
rescue ::Gitlab::CIDR::ValidationError => e
errors.add(:geo_node_allowed_ips, e.message)
end
end end
end end
...@@ -13,15 +13,17 @@ ...@@ -13,15 +13,17 @@
= form_errors(@application_setting) = form_errors(@application_setting)
%fieldset %fieldset
%p
These settings will only take effect if Geo is enabled and require a restart to take effect.
.form-group .form-group
= f.label :geo_status_timeout, 'Connection timeout', class: 'label-bold' = f.label :geo_status_timeout, 'Connection timeout', class: 'label-bold'
= f.number_field :geo_status_timeout, class: 'form-control' = f.number_field :geo_status_timeout, class: 'form-control'
.form-text.text-muted .form-text.text-muted
The amount of seconds after which a request to get a secondary node = _('The amount of seconds after which a request to get a secondary node status will time out.')
status will time out. .form-group
= f.label :geo_node_allowed_ips, 'Allowed Geo IP', class: 'label-bold'
= f.text_field :geo_node_allowed_ips, class: 'form-control'
.form-text.text-muted
= _('List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. "1.1.1.1, 2.2.2.0/24"')
= f.submit 'Save changes', class: "btn btn-success" = f.submit _('Save changes'), class: "btn btn-success"
- else - else
= render 'shared/empty_states/geo' = render 'shared/empty_states/geo'
---
title: IP whitelisting for Geo-enabling functionality in the primary
merge_request: 10383
author:
type: added
# frozen_string_literal: true
class AddAllowedGeoIPsToApplicationSettings < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :geo_node_allowed_ips, :string, default: '0.0.0.0/0, ::/0'
end
end
...@@ -26,6 +26,8 @@ module API ...@@ -26,6 +26,8 @@ module API
requires :id, type: Integer, desc: 'The DB ID of the file' requires :id, type: Integer, desc: 'The DB ID of the file'
end end
get 'transfers/:type/:id' do get 'transfers/:type/:id' do
check_gitlab_geo_request_ip!
service = ::Geo::FileUploadService.new(params, headers['Authorization']) service = ::Geo::FileUploadService.new(params, headers['Authorization'])
response = service.execute response = service.execute
...@@ -44,6 +46,7 @@ module API ...@@ -44,6 +46,7 @@ module API
# Example request: # Example request:
# POST /geo/status # POST /geo/status
post 'status' do post 'status' do
check_gitlab_geo_request_ip!
authenticate_by_gitlab_geo_node_token! authenticate_by_gitlab_geo_node_token!
db_status = GeoNode.find(params[:geo_node_id]).find_or_build_status db_status = GeoNode.find(params[:geo_node_id]).find_or_build_status
......
...@@ -20,6 +20,10 @@ module EE ...@@ -20,6 +20,10 @@ module EE
render_api_error!(e.to_s, 401) render_api_error!(e.to_s, 401)
end end
def check_gitlab_geo_request_ip!
unauthorized! unless ::Gitlab::Geo.allowed_ip?(request.ip)
end
override :current_user override :current_user
def current_user def current_user
strong_memoize(:current_user) do strong_memoize(:current_user) do
......
# rubocop:disable Naming/FileName
# frozen_string_literal: true
require 'ipaddr'
module Gitlab
class CIDR
ValidationError = Class.new(StandardError)
attr_reader :cidrs
def initialize(values)
@cidrs = parse_cidrs(values)
end
def match?(ip)
cidrs.find { |cidr| cidr.include?(ip) }.present?
end
private
def parse_cidrs(values)
values.to_s.split(',').map do |value|
::IPAddr.new(value.strip)
end
rescue => e
raise ValidationError, e.message
end
end
end
...@@ -132,5 +132,11 @@ module Gitlab ...@@ -132,5 +132,11 @@ module Gitlab
true true
end end
end end
def self.allowed_ip?(ip)
allowed_ips = ::Gitlab::CurrentSettings.current_application_settings.geo_node_allowed_ips
Gitlab::CIDR.new(allowed_ips).match?(ip)
end
end end
end end
...@@ -34,7 +34,8 @@ describe Admin::ApplicationSettingsController do ...@@ -34,7 +34,8 @@ describe Admin::ApplicationSettingsController do
slack_app_id: 'slack_app_id', slack_app_id: 'slack_app_id',
slack_app_secret: 'slack_app_secret', slack_app_secret: 'slack_app_secret',
slack_app_verification_token: 'slack_app_verification_token', slack_app_verification_token: 'slack_app_verification_token',
allow_group_owners_to_manage_ldap: false allow_group_owners_to_manage_ldap: false,
geo_node_allowed_ips: '0.0.0.0/0, ::/0'
} }
put :update, params: { application_setting: settings } put :update, params: { application_setting: settings }
...@@ -48,7 +49,7 @@ describe Admin::ApplicationSettingsController do ...@@ -48,7 +49,7 @@ describe Admin::ApplicationSettingsController do
end end
shared_examples 'settings for licensed features' do shared_examples 'settings for licensed features' do
it 'does not update settings when licesed feature is not available' do it 'does not update settings when licensed feature is not available' do
stub_licensed_features(feature => false) stub_licensed_features(feature => false)
attribute_names = settings.keys.map(&:to_s) attribute_names = settings.keys.map(&:to_s)
......
...@@ -15,10 +15,12 @@ describe 'Admin updates EE-only settings' do ...@@ -15,10 +15,12 @@ describe 'Admin updates EE-only settings' do
visit geo_admin_application_settings_path visit geo_admin_application_settings_path
page.within('.as-geo') do page.within('.as-geo') do
fill_in 'Connection timeout', with: 15 fill_in 'Connection timeout', with: 15
fill_in 'Allowed Geo IP', with: '192.34.34.34'
click_button 'Save changes' click_button 'Save changes'
end end
expect(Gitlab::CurrentSettings.geo_status_timeout).to eq(15) expect(Gitlab::CurrentSettings.geo_status_timeout).to eq(15)
expect(Gitlab::CurrentSettings.geo_node_allowed_ips).to eq('192.34.34.34')
expect(page).to have_content "Application settings saved successfully" expect(page).to have_content "Application settings saved successfully"
end end
end end
......
require 'spec_helper'
describe Gitlab::CIDR do
using RSpec::Parameterized::TableSyntax
context 'validation' do
it 'raises an exception when an octet is invalid' do
expect { described_class.new("192.1.1.257") }.to raise_error(described_class::ValidationError)
end
it 'raises an exception when a bitmask is invalid' do
expect { described_class.new("192.1.1.0/34") }.to raise_error(described_class::ValidationError)
end
it 'raises an exception when one IP from a set is invalid' do
expect { described_class.new("192.1.1.257, 192.1.1.1") }.to raise_error(described_class::ValidationError)
end
end
context 'matching' do
where(:values, :ip, :match) do
"192.1.1.1" | "192.1.1.1" | true
"192.1.1.1, 192.1.2.1" | "192.1.2.1" | true
"192.1.1.0/24" | "192.1.1.223" | true
"192.1.0.0/16" | "192.1.223.223" | true
"192.1.0.0/16, 192.1.2.0/24" | "192.1.2.223" | true
"192.1.0.0/16" | "192.2.1.1" | false
"192.1.0.1" | "192.2.1.1" | false
end
with_them do
it do
expect(described_class.new(values).match?(ip)).to eq(match)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Geo, :geo, :request_store do describe Gitlab::Geo, :geo, :request_store do
using RSpec::Parameterized::TableSyntax
include ::EE::GeoHelpers include ::EE::GeoHelpers
set(:primary_node) { create(:geo_node, :primary) } set(:primary_node) { create(:geo_node, :primary) }
...@@ -242,4 +243,24 @@ describe Gitlab::Geo, :geo, :request_store do ...@@ -242,4 +243,24 @@ describe Gitlab::Geo, :geo, :request_store do
end end
end end
end end
context '.allowed_ip?' do
where(:allowed_ips, :ip, :allowed) do
"192.1.1.1" | "192.1.1.1" | true
"192.1.1.1, 192.1.2.1" | "192.1.2.1" | true
"192.1.1.0/24" | "192.1.1.223" | true
"192.1.0.0/16" | "192.1.223.223" | true
"192.1.0.0/16, 192.1.2.0/24" | "192.1.2.223" | true
"192.1.0.0/16" | "192.2.1.1" | false
"192.1.0.1" | "192.2.1.1" | false
end
with_them do
it do
stub_application_setting(geo_node_allowed_ips: allowed_ips)
expect(described_class.allowed_ip?(ip)).to eq(allowed)
end
end
end
end end
require 'spec_helper' require 'spec_helper'
describe ApplicationSetting do describe ApplicationSetting do
using RSpec::Parameterized::TableSyntax
subject(:setting) { described_class.create_from_defaults } subject(:setting) { described_class.create_from_defaults }
describe 'validations' do describe 'validations' do
...@@ -80,6 +82,27 @@ describe ApplicationSetting do ...@@ -80,6 +82,27 @@ describe ApplicationSetting do
end end
end end
end end
context 'when validating allowed_ips' do
where(:allowed_ips, :is_valid) do
"192.1.1.1" | true
"192.1.1.0/24" | true
"192.1.1.0/24, 192.1.20.23" | true
"192.1.1.0/24, 192.23.0.0/16" | true
"192.1.1.0/34" | false
"192.1.1.257" | false
"192.1.1.257, 192.1.1.1" | false
"300.1.1.0/34" | false
end
with_them do
it do
setting.update_column(:geo_node_allowed_ips, allowed_ips)
expect(setting.reload.valid?).to eq(is_valid)
end
end
end
end end
describe '#should_check_namespace_plan?' do describe '#should_check_namespace_plan?' do
......
...@@ -34,6 +34,29 @@ describe API::Geo do ...@@ -34,6 +34,29 @@ describe API::Geo do
stub_current_geo_node(secondary_node) stub_current_geo_node(secondary_node)
end end
describe 'allowed IPs' do
let(:note) { create(:note, :with_attachment) }
let(:upload) { Upload.find_by(model: note, uploader: 'AttachmentUploader') }
let(:transfer) { Gitlab::Geo::FileTransfer.new(:attachment, upload) }
let(:req_header) { Gitlab::Geo::TransferRequest.new(transfer.request_data).headers }
it 'responds with 401 when IP is not allowed' do
stub_application_setting(geo_node_allowed_ips: '192.34.34.34')
get api("/geo/transfers/attachment/#{upload.id}"), headers: req_header
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 200 when IP is allowed' do
stub_application_setting(geo_node_allowed_ips: '127.0.0.1')
get api("/geo/transfers/attachment/#{upload.id}"), headers: req_header
expect(response).to have_gitlab_http_status(200)
end
end
describe 'GET /geo/transfers/attachment/1' do describe 'GET /geo/transfers/attachment/1' do
let(:note) { create(:note, :with_attachment) } let(:note) { create(:note, :with_attachment) }
let(:upload) { Upload.find_by(model: note, uploader: 'AttachmentUploader') } let(:upload) { Upload.find_by(model: note, uploader: 'AttachmentUploader') }
...@@ -272,6 +295,24 @@ describe API::Geo do ...@@ -272,6 +295,24 @@ describe API::Geo do
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(401)
end end
describe 'allowed IPs' do
it 'responds with 401 when IP is not allowed' do
stub_application_setting(geo_node_allowed_ips: '192.34.34.34')
request
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 201 when IP is allowed' do
stub_application_setting(geo_node_allowed_ips: '127.0.0.1')
request
expect(response).to have_gitlab_http_status(201)
end
end
context 'when requesting primary node with valid auth header' do context 'when requesting primary node with valid auth header' do
before do before do
stub_current_geo_node(primary_node) stub_current_geo_node(primary_node)
......
...@@ -394,6 +394,32 @@ describe "Git HTTP requests (Geo)", :geo do ...@@ -394,6 +394,32 @@ describe "Git HTTP requests (Geo)", :geo do
end end
end end
end end
context 'IP allowed settings' do
subject do
make_request
response
end
def make_request
get "/#{repository_path}.git/info/refs", params: { service: 'git-upload-pack' }, headers: env
end
let(:repository_path) { project.full_path }
it 'returns unauthorized error' do
stub_application_setting(geo_node_allowed_ips: '192.34.34.34')
is_expected.to have_gitlab_http_status(:unauthorized)
expect(subject.parsed_body).to eq('Request from this IP is not allowed')
end
it 'returns success response' do
stub_application_setting(geo_node_allowed_ips: '127.0.0.1')
is_expected.to have_gitlab_http_status(:success)
end
end
end end
def valid_geo_env def valid_geo_env
......
...@@ -6449,6 +6449,9 @@ msgstr "" ...@@ -6449,6 +6449,9 @@ msgstr ""
msgid "List available repositories" msgid "List available repositories"
msgstr "" msgstr ""
msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \"1.1.1.1, 2.2.2.0/24\""
msgstr ""
msgid "List view" msgid "List view"
msgstr "" msgstr ""
...@@ -10527,6 +10530,9 @@ msgstr "" ...@@ -10527,6 +10530,9 @@ msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS." msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr "" msgstr ""
msgid "The amount of seconds after which a request to get a secondary node status will time out."
msgstr ""
msgid "The branch for this project has no active pipeline configuration." msgid "The branch for this project has no active pipeline configuration."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment