diff --git a/ee/lib/api/geo.rb b/ee/lib/api/geo.rb index 18a8444023fe2db9000a844d64095a38e32dc0a5..167ee6301bf82466919c18ba7d9a5716c17ae512 100644 --- a/ee/lib/api/geo.rb +++ b/ee/lib/api/geo.rb @@ -1,3 +1,5 @@ +require 'base64' + module API class Geo < Grape::API resource :geo do @@ -40,6 +42,51 @@ class Geo < Grape::API render_validation_error!(db_status) end end + + # git push over SSH secondary -> primary related proxying logic + # + resource 'proxy_git_push_ssh' do + format :json + + # Responsible for making HTTP GET /repo.git/info/refs?service=git-receive-pack + # request *from* secondary gitlab-shell to primary + # + params do + requires :secret_token, type: String + requires :data, type: Hash do + requires :gl_id, type: String + requires :primary_repo, type: String + end + end + post 'info_refs' do + authenticate_by_gitlab_shell_token! + params.delete(:secret_token) + + resp = Gitlab::Geo::GitPushSSHProxy.new(params['data']).info_refs + status(resp.code.to_i) + { status: true, message: nil, result: Base64.encode64(resp.body.to_s) } + end + + # Responsible for making HTTP POST /repo.git/git-receive-pack + # request *from* secondary gitlab-shell to primary + # + params do + requires :secret_token, type: String + requires :data, type: Hash do + requires :gl_id, type: String + requires :primary_repo, type: String + end + requires :output, type: String, desc: 'Output from git-receive-pack' + end + post 'push' do + authenticate_by_gitlab_shell_token! + params.delete(:secret_token) + + resp = Gitlab::Geo::GitPushSSHProxy.new(params['data']).push(Base64.decode64(params['output'])) + status(resp.code.to_i) + { status: true, message: nil, result: Base64.encode64(resp.body.to_s) } + end + end end end end diff --git a/ee/lib/ee/gitlab/middleware/read_only/controller.rb b/ee/lib/ee/gitlab/middleware/read_only/controller.rb index a8705a706ac14119bc1bfcf1d18994ee1309cb0b..0d7c04497d92cac0db23b4a22871e95e3f4de9cb 100644 --- a/ee/lib/ee/gitlab/middleware/read_only/controller.rb +++ b/ee/lib/ee/gitlab/middleware/read_only/controller.rb @@ -17,7 +17,7 @@ module Controller override :whitelisted_routes def whitelisted_routes - super || geo_node_update_route + super || geo_node_update_route || geo_proxy_git_push_ssh_route end def geo_node_update_route @@ -33,6 +33,14 @@ def geo_node_update_route WHITELISTED_GEO_ROUTES_TRACKING_DB[controller]&.include?(action) end end + + def geo_proxy_git_push_ssh_route + routes = ::Gitlab::Middleware::ReadOnly::API_VERSIONS.map do |version| + ["/api/v#{version}/geo/proxy_git_push_ssh/info_refs", + "/api/v#{version}/geo/proxy_git_push_ssh/push"] + end + routes.flatten.include?(request.path) + end end end end diff --git a/ee/spec/requests/api/geo_spec.rb b/ee/spec/requests/api/geo_spec.rb index d95bb7bbd84263803d5b631f09cd67262f29f57a..a917d007eded17a431348f8f2137589d08f662b1 100644 --- a/ee/spec/requests/api/geo_spec.rb +++ b/ee/spec/requests/api/geo_spec.rb @@ -25,7 +25,7 @@ end end - describe '/geo/transfers' do + describe 'GET /geo/transfers' do before do stub_current_geo_node(secondary_node) end @@ -287,4 +287,119 @@ it_behaves_like 'with terms enforced' end end + + describe '/geo/proxy_git_push_ssh' do + let(:secret_token) { Gitlab::Shell.secret_token } + let(:data) { { primary_repo: 'http://localhost:3001/testuser/repo.git', gl_id: 'key-1', gl_username: 'testuser' } } + + before do + stub_current_geo_node(secondary_node) + end + + describe 'POST /geo/proxy_git_push_ssh/info_refs' do + context 'with all required params missing' do + it 'responds with 400' do + post api('/geo/proxy_git_push_ssh/info_refs'), nil + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eql('secret_token is missing, data is missing, data[gl_id] is missing, data[primary_repo] is missing') + end + end + + context 'with all required params' do + let(:git_push_ssh_proxy) { double(Gitlab::Geo::GitPushSSHProxy) } + + before do + allow(Gitlab::Geo::GitPushSSHProxy).to receive(:new).with(data).and_return(git_push_ssh_proxy) + end + + context 'with an invalid secret_token' do + it 'responds with 401' do + post(api('/geo/proxy_git_push_ssh/info_refs'), { secret_token: 'invalid', data: data }) + + expect(response).to have_gitlab_http_status(401) + expect(json_response['error']).to be_nil + end + end + + context 'where an exception occurs' do + it 'responds with 500' do + expect(git_push_ssh_proxy).to receive(:info_refs).and_raise('deliberate exception raised') + + post api('/geo/proxy_git_push_ssh/info_refs'), { secret_token: secret_token, data: data } + + expect(response).to have_gitlab_http_status(500) + expect(json_response['message']).to include('RuntimeError (deliberate exception raised)') + expect(json_response['result']).to be_nil + end + end + + context 'with a valid secret token' do + let(:http_response) { double(Net::HTTPResponse, code: 200, body: 'something here') } + + it 'responds with 200' do + expect(git_push_ssh_proxy).to receive(:info_refs).and_return(http_response) + + post api('/geo/proxy_git_push_ssh/info_refs'), { secret_token: secret_token, data: data } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['result']).to eql('something+here') + end + end + end + end + + describe 'POST /geo/proxy_git_push_ssh/push' do + context 'with all required params missing' do + it 'responds with 400' do + post api('/geo/proxy_git_push_ssh/push'), nil + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eql('secret_token is missing, data is missing, data[gl_id] is missing, data[primary_repo] is missing, output is missing') + end + end + + context 'with all required params' do + let(:output) { 'output text'} + let(:git_push_ssh_proxy) { double(Gitlab::Geo::GitPushSSHProxy) } + + before do + allow(Gitlab::Geo::GitPushSSHProxy).to receive(:new).with(data).and_return(git_push_ssh_proxy) + end + + context 'with an invalid secret_token' do + it 'responds with 401' do + post(api('/geo/proxy_git_push_ssh/push'), { secret_token: 'invalid', data: data, output: output }) + + expect(response).to have_gitlab_http_status(401) + expect(json_response['error']).to be_nil + end + end + + context 'where an exception occurs' do + it 'responds with 500' do + expect(git_push_ssh_proxy).to receive(:push).and_raise('deliberate exception raised') + post api('/geo/proxy_git_push_ssh/push'), { secret_token: secret_token, data: data, output: output } + + expect(response).to have_gitlab_http_status(500) + expect(json_response['message']).to include('RuntimeError (deliberate exception raised)') + expect(json_response['result']).to be_nil + end + end + + context 'with a valid secret token' do + let(:http_response) { double(Net::HTTPResponse, code: 201, body: 'something here') } + + it 'responds with 201' do + expect(git_push_ssh_proxy).to receive(:push).with(output).and_return(http_response) + + post api('/geo/proxy_git_push_ssh/push'), { secret_token: secret_token, data: data, output: output } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['result']).to eql('something+here') + end + end + end + end + end end