Commit 7efdde6b authored by Gabriel Mazetto's avatar Gabriel Mazetto

Merge branch 'gitlab-geo-middleware' into 'master'

Gitlab Geo: Middleware

Implements a Rails Middleware to prevent potential writing operations on the Readonly node of Gitlab Geo (https://gitlab.com/gitlab-org/gitlab-ee/issues/76).

We will redirect back to last visited page or root_url when any operation with Request type "POST PATCH PUT DELETE".

This is a special measure to friendly disallow potential dataloss or fatal errors while trying to change a readonly database

See merge request !139
parents 9b5e4b69 89492c90
......@@ -105,5 +105,8 @@ module Gitlab
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
# Gitlab Geo Middleware support
config.middleware.use 'Gitlab::Middleware::ReadonlyGeo'
end
end
module Gitlab
module Middleware
class ReadonlyGeo
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE)
def initialize(app)
@app = app
end
def call(env)
@env = env
if disallowed_request? && Gitlab::Geo.readonly?
Rails.logger.debug('Gitlab Geo: preventing possible non readonly operation')
rack_flash.alert = 'You cannot do writing operations on a readonly Gitlab Geo instance'
rack_session['flash'] = rack_flash.to_session_value
return [301, { 'Location' => last_visited_url }, []]
end
@app.call(env)
end
private
def disallowed_request?
DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !logout_route
end
def rack_flash
@rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
end
def rack_session
@env['rack.session']
end
def request
@request ||= Rack::Request.new(@env)
end
def last_visited_url
@env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url
end
def route_hash
@route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
end
def logout_route
route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
end
end
end
end
require 'spec_helper'
describe Gitlab::Middleware::ReadonlyGeo, lib: true do
include Rack::Test::Methods
RSpec::Matchers.define :be_a_redirect do
match do |response|
response.status == 301
end
end
RSpec::Matchers.define :disallow_request do
match do |middleware|
flash = middleware.send(:rack_flash)
flash['alert'] && flash['alert'].include?('You cannot do writing operations')
end
end
let(:rack_stack) do
rack = Rack::Builder.new do
use ActionDispatch::Session::CacheStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
end
rack.run(subject)
rack.to_app
end
subject { described_class.new(fake_app) }
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
let(:request) { @request ||= Rack::MockRequest.new(rack_stack) }
context 'when in Gitlab Geo readonly node' do
before(:each) { allow(Gitlab::Geo).to receive(:readonly?) { true } }
it 'expects PATCH requests to be disallowed' do
response = request.patch('/test_request')
expect(response).to be_a_redirect
expect(subject).to disallow_request
end
it 'expects PUT requests to be disallowed' do
response = request.put('/test_request')
expect(response).to be_a_redirect
expect(subject).to disallow_request
end
it 'expects POST requests to be disallowed' do
response = request.post('/test_request')
expect(response).to be_a_redirect
expect(subject).to disallow_request
end
it 'expects DELETE requests to be disallowed' do
response = request.delete('/test_request')
expect(response).to be_a_redirect
expect(subject).to disallow_request
end
it 'expects DELETE request to logout to be allowed' do
response = request.delete('/users/sign_out')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
end
end
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