Commit d1366971 authored by Toon Claes's avatar Toon Claes

Create idea of read-only database

In GitLab EE, a GitLab instance can be read-only (e.g. when it's a Geo
secondary node). But in GitLab CE it also might be useful to have the
"read-only" idea around. So port it back to GitLab CE.

Also having the principle of read-only in GitLab CE would hopefully
lead to less errors introduced, doing write operations when there
aren't allowed for read-only calls.

Closes gitlab-org/gitlab-ce#37534.
parent 2cf5dca8
...@@ -3,9 +3,23 @@ ...@@ -3,9 +3,23 @@
# Automatically sets the layout and ensures an administrator is logged in # Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin! before_action :authenticate_admin!
before_action :display_read_only_information
layout 'admin' layout 'admin'
def authenticate_admin! def authenticate_admin!
render_404 unless current_user.admin? render_404 unless current_user.admin?
end end
def display_read_only_information
return unless Gitlab::Database.read_only?
flash.now[:notice] = read_only_message
end
private
# Overridden in EE
def read_only_message
_('You are on a read-only GitLab instance.')
end
end end
...@@ -10,7 +10,7 @@ module Boards ...@@ -10,7 +10,7 @@ module Boards
def index def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20) issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues) make_sure_position_is_set(issues) if Gitlab::Database.read_write?
issues = issues.preload(:project, issues = issues.preload(:project,
:milestone, :milestone,
:assignees, :assignees,
......
...@@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest include LfsRequest
skip_before_action :lfs_check_access!, only: [:deprecated] skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
def batch def batch
unless objects.present? unless objects.present?
...@@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController
} }
} }
end end
def lfs_check_batch_operation!
if upload_request? && Gitlab::Database.read_only?
render(
json: {
message: lfs_read_only_message
},
content_type: 'application/vnd.git-lfs+json',
status: 403
)
end
end
# Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
end end
...@@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
# Make sure merge requests created before 8.0 # Make sure merge requests created before 8.0
# have head file in refs/merge-requests/ # have head file in refs/merge-requests/
def ensure_ref_fetched def ensure_ref_fetched
@merge_request.ensure_ref_fetched @merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end end
def merge_request_params def merge_request_params
......
...@@ -8,8 +8,7 @@ class SessionsController < Devise::SessionsController ...@@ -8,8 +8,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create] if: :two_factor_enabled?, only: [:create]
prepend_before_action :store_redirect_path, only: [:new] prepend_before_action :store_redirect_uri, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new] before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha before_action :load_recaptcha
...@@ -86,28 +85,36 @@ class SessionsController < Devise::SessionsController ...@@ -86,28 +85,36 @@ class SessionsController < Devise::SessionsController
end end
end end
def store_redirect_path def stored_redirect_uri
redirect_path = @redirect_to ||= stored_location_for(:redirect)
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host
referer_uri.request_uri
else
request.fullpath
end end
def store_redirect_uri
redirect_uri =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
URI(request.referer)
else else
request.fullpath URI(request.url)
end end
# Prevent a 'you are already signed in' message directly after signing: # Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully. # we should never redirect to '/users/sign_in' after signing in successfully.
unless URI(redirect_path).path == new_user_session_path return true if redirect_uri.path == new_user_session_path
store_location_for(:redirect, redirect_path)
redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
@redirect_to = redirect_to
store_location_for(:redirect, redirect_to)
end end
# Overridden in EE
def redirect_allowed_to?(uri)
uri.host == Gitlab.config.gitlab.host &&
uri.port == Gitlab.config.gitlab.port
end end
def two_factor_enabled? def two_factor_enabled?
find_user.try(:two_factor_enabled?) find_user&.two_factor_enabled?
end end
def auto_sign_in_with_provider def auto_sign_in_with_provider
......
...@@ -59,7 +59,7 @@ module CacheMarkdownField ...@@ -59,7 +59,7 @@ module CacheMarkdownField
# Update every column in a row if any one is invalidated, as we only store # Update every column in a row if any one is invalidated, as we only store
# one version per row # one version per row
def refresh_markdown_cache!(do_update: false) def refresh_markdown_cache
options = { skip_project_check: skip_project_check? } options = { skip_project_check: skip_project_check? }
updates = cached_markdown_fields.markdown_fields.map do |markdown_field| updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
...@@ -71,8 +71,14 @@ module CacheMarkdownField ...@@ -71,8 +71,14 @@ module CacheMarkdownField
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
updates.each {|html_field, data| write_attribute(html_field, data) } updates.each {|html_field, data| write_attribute(html_field, data) }
end
def refresh_markdown_cache!
updates = refresh_markdown_cache
return unless persisted? && Gitlab::Database.read_write?
update_columns(updates) if persisted? && do_update update_columns(updates)
end end
def cached_html_up_to_date?(markdown_field) def cached_html_up_to_date?(markdown_field)
...@@ -124,8 +130,8 @@ module CacheMarkdownField ...@@ -124,8 +130,8 @@ module CacheMarkdownField
end end
# Using before_update here conflicts with elasticsearch-model somehow # Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache? before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache? before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
end end
class_methods do class_methods do
......
...@@ -156,6 +156,8 @@ module Routable ...@@ -156,6 +156,8 @@ module Routable
end end
def update_route def update_route
return if Gitlab::Database.read_only?
prepare_route prepare_route
route.save route.save
end end
......
...@@ -43,15 +43,17 @@ module TokenAuthenticatable ...@@ -43,15 +43,17 @@ module TokenAuthenticatable
write_attribute(token_field, token) if token write_attribute(token_field, token) if token
end end
# Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
read_attribute(token_field) read_attribute(token_field)
end end
# Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do define_method("reset_#{token_field}!") do
write_new_token(token_field) write_new_token(token_field)
save! save! if Gitlab::Database.read_write?
end end
end end
end end
......
...@@ -477,7 +477,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -477,7 +477,7 @@ class MergeRequest < ActiveRecord::Base
end end
def check_if_can_be_merged def check_if_can_be_merged
return unless unchecked? return unless unchecked? && Gitlab::Database.read_write?
can_be_merged = can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
......
...@@ -814,7 +814,7 @@ class Project < ActiveRecord::Base ...@@ -814,7 +814,7 @@ class Project < ActiveRecord::Base
end end
def cache_has_external_issue_tracker def cache_has_external_issue_tracker
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
end end
def has_wiki? def has_wiki?
...@@ -834,7 +834,7 @@ class Project < ActiveRecord::Base ...@@ -834,7 +834,7 @@ class Project < ActiveRecord::Base
end end
def cache_has_external_wiki def cache_has_external_wiki
update_column(:has_external_wiki, services.external_wikis.any?) update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
end end
def find_or_initialize_services(exceptions: []) def find_or_initialize_services(exceptions: [])
......
...@@ -459,6 +459,14 @@ class User < ActiveRecord::Base ...@@ -459,6 +459,14 @@ class User < ActiveRecord::Base
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end end
def remember_me!
super if ::Gitlab::Database.read_write?
end
def forget_me!
super if ::Gitlab::Database.read_write?
end
def disable_two_factor! def disable_two_factor!
transaction do transaction do
update_attributes( update_attributes(
......
...@@ -16,6 +16,8 @@ module Keys ...@@ -16,6 +16,8 @@ module Keys
end end
def update? def update?
return false if ::Gitlab::Database.read_only?
last_used = key.last_used_at last_used = key.last_used_at
return false if last_used && (Time.zone.now - last_used) <= TIMEOUT return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
......
...@@ -14,7 +14,7 @@ module Users ...@@ -14,7 +14,7 @@ module Users
private private
def record_activity def record_activity
Gitlab::UserActivities.record(@author.id) Gitlab::UserActivities.record(@author.id) if Gitlab::Database.read_write?
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})") Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end end
......
---
title: Create idea of read-only database
merge_request: 14688
author:
type: changed
...@@ -154,6 +154,9 @@ module Gitlab ...@@ -154,6 +154,9 @@ module Gitlab
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
ENV['GIT_TERMINAL_PROMPT'] = '0' ENV['GIT_TERMINAL_PROMPT'] = '0'
# Gitlab Read-only middleware support
config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
config.generators do |g| config.generators do |g|
g.factory_girl false g.factory_girl false
end end
......
...@@ -24,3 +24,15 @@ else ...@@ -24,3 +24,15 @@ else
run_query run_query
end end
``` ```
# Read-only database
The database can be used in read-only mode. In this case we have to
make sure all GET requests don't attempt any write operations to the
database. If one of those requests wants to write to the database, it needs
to be wrapped in a `Gitlab::Database.read_only?` or `Gitlab::Database.read_write?`
guard, to make sure it doesn't for read-only databases.
We have a Rails Middleware that filters any potentially writing
operations (the CUD operations of CRUD) and prevent the user from trying
to update the database and getting a 500 error (see `Gitlab::Middleware::ReadOnly`).
...@@ -40,7 +40,7 @@ module Banzai ...@@ -40,7 +40,7 @@ module Banzai
return cacheless_render_field(object, field) return cacheless_render_field(object, field)
end end
object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field) object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
object.cached_html_for(field) object.cached_html_for(field)
end end
...@@ -162,10 +162,5 @@ module Banzai ...@@ -162,10 +162,5 @@ module Banzai
return unless cache_key return unless cache_key
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end end
# GitLab EE needs to disable updates on GET requests in Geo
def self.update_object?(object)
true
end
end end
end end
...@@ -29,6 +29,15 @@ module Gitlab ...@@ -29,6 +29,15 @@ module Gitlab
adapter_name.casecmp('postgresql').zero? adapter_name.casecmp('postgresql').zero?
end end
# Overridden in EE
def self.read_only?
false
end
def self.read_write?
!self.read_only?
end
def self.version def self.version
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end end
......
...@@ -17,7 +17,8 @@ module Gitlab ...@@ -17,7 +17,8 @@ module Gitlab
command_not_allowed: "The command you're trying to execute is not allowed.", command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.', upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.', receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
readonly: 'The repository is temporarily read-only. Please try again later.' read_only: 'The repository is temporarily read-only. Please try again later.',
cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze }.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
...@@ -161,7 +162,11 @@ module Gitlab ...@@ -161,7 +162,11 @@ module Gitlab
def check_push_access!(changes) def check_push_access!(changes)
if project.repository_read_only? if project.repository_read_only?
raise UnauthorizedError, ERROR_MESSAGES[:readonly] raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end
if Gitlab::Database.read_only?
raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only]
end end
if deploy_key if deploy_key
......
module Gitlab module Gitlab
class GitAccessWiki < GitAccess class GitAccessWiki < GitAccess
ERROR_MESSAGES = { ERROR_MESSAGES = {
read_only: "You can't push code to a read-only GitLab instance.",
write_to_wiki: "You are not allowed to write to this project's wiki." write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze }.freeze
...@@ -17,6 +18,10 @@ module Gitlab ...@@ -17,6 +18,10 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki] raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end end
if Gitlab::Database.read_only?
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end
true true
end end
end end
......
module Gitlab
module Middleware
class ReadOnly
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'.freeze
API_VERSIONS = (3..4)
def initialize(app)
@app = app
@whitelisted = internal_routes
end
def call(env)
@env = env
if disallowed_request? && Gitlab::Database.read_only?
Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
error_message = 'You cannot do writing operations on a read-only GitLab instance'
if json_request?
return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
else
rack_flash.alert = error_message
rack_session['flash'] = rack_flash.to_session_value
return [301, { 'Location' => last_visited_url }, []]
end
end
@app.call(env)
end
private
def internal_routes
API_VERSIONS.flat_map { |version| "api/v#{version}/internal" }
end
def disallowed_request?
DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !whitelisted_routes
end
def json_request?
request.media_type == APPLICATION_JSON
end
def rack_flash
@rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
end
def rack_session
@env['rack.session']
end
def request
@env['rack.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 whitelisted_routes
logout_route || grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end
def logout_route
route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
end
def sidekiq_route
request.path.start_with?('/admin/sidekiq')
end
def grack_route
request.path.end_with?('.git/git-upload-pack')
end
def lfs_route
request.path.end_with?('/info/lfs/objects/batch')
end
end
end
end
...@@ -11,10 +11,10 @@ module SystemCheck ...@@ -11,10 +11,10 @@ module SystemCheck
].freeze ].freeze
set_name 'Git user has default SSH configuration?' set_name 'Git user has default SSH configuration?'
set_skip_reason 'skipped (git user is not present or configured)' set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
def skip? def skip?
!home_dir || !File.directory?(home_dir) Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
end end
def check? def check?
......
...@@ -149,7 +149,7 @@ FactoryGirl.define do ...@@ -149,7 +149,7 @@ FactoryGirl.define do
end end
end end
trait :readonly do trait :read_only do
repository_read_only true repository_read_only true
end end
......
...@@ -31,7 +31,14 @@ describe Banzai::Renderer do ...@@ -31,7 +31,14 @@ describe Banzai::Renderer do
let(:object) { fake_object(fresh: false) } let(:object) { fake_object(fresh: false) }
it 'caches and returns the result' do it 'caches and returns the result' do
expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) expect(object).to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html')
end
it "skips database caching on a GitLab read-only instance" do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect(object).to receive(:refresh_markdown_cache!)
is_expected.to eq('field_html') is_expected.to eq('field_html')
end end
......
...@@ -598,6 +598,19 @@ describe Gitlab::GitAccess do ...@@ -598,6 +598,19 @@ describe Gitlab::GitAccess do
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
end end
end end
context "when in a read-only GitLab instance" do
before do
create(:protected_branch, name: 'feature', project: project)
allow(Gitlab::Database).to receive(:read_only?) { true }
end
# Only check admin; if an admin can't do it, other roles can't either
matrix = permissions_matrix[:admin].dup
matrix.each { |key, _| matrix[key] = false }
run_permission_checks(admin: matrix)
end
end end
describe 'build authentication abilities' do describe 'build authentication abilities' do
...@@ -632,6 +645,16 @@ describe Gitlab::GitAccess do ...@@ -632,6 +645,16 @@ describe Gitlab::GitAccess do
end end
end end
context 'when the repository is read only' do
let(:project) { create(:project, :repository, :read_only) }
it 'denies push access' do
project.add_master(user)
expect { push_access_check }.to raise_unauthorized('The repository is temporarily read-only. Please try again later.')
end
end
describe 'deploy key permissions' do describe 'deploy key permissions' do
let(:key) { create(:deploy_key, user: user, can_push: can_push) } let(:key) { create(:deploy_key, user: user, can_push: can_push) }
let(:actor) { key } let(:actor) { key }
......
...@@ -4,6 +4,7 @@ describe Gitlab::GitAccessWiki do ...@@ -4,6 +4,7 @@ describe Gitlab::GitAccessWiki do
let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
let(:redirected_path) { nil } let(:redirected_path) { nil }
let(:authentication_abilities) do let(:authentication_abilities) do
[ [
...@@ -13,7 +14,8 @@ describe Gitlab::GitAccessWiki do ...@@ -13,7 +14,8 @@ describe Gitlab::GitAccessWiki do
] ]
end end
describe 'push_allowed?' do describe '#push_access_check' do
context 'when user can :create_wiki' do
before do before do
create(:protected_branch, name: 'master', project: project) create(:protected_branch, name: 'master', project: project)
project.team << [user, :developer] project.team << [user, :developer]
...@@ -22,10 +24,17 @@ describe Gitlab::GitAccessWiki do ...@@ -22,10 +24,17 @@ describe Gitlab::GitAccessWiki do
subject { access.check('git-receive-pack', changes) } subject { access.check('git-receive-pack', changes) }
it { expect { subject }.not_to raise_error } it { expect { subject }.not_to raise_error }
context 'when in a read-only GitLab instance' do
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
end end
def changes it 'does not give access to upload wiki code' do
['6f6d7e7ed 570e7b2ab refs/heads/master'] expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.")
end
end
end
end end
describe '#access_check_download!' do describe '#access_check_download!' do
......
require 'spec_helper'
describe Gitlab::Middleware::ReadOnly 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
RSpec::Matchers.define :disallow_request_in_json do
match do |response|
json_response = JSON.parse(response.body)
response.body.include?('You cannot do writing operations') && json_response.key?('message')
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(:request) { Rack::MockRequest.new(rack_stack) }
context 'normal requests to a read-only Gitlab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
end
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 a internal POST request to be allowed after a disallowed request' do
response = request.post('/test_request')
expect(response).to be_a_redirect
response = request.post("/api/#{API::API.version}/internal")
expect(response).not_to be_a_redirect
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
context 'whitelisted requests' do
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
it 'expects a POST internal request to be allowed' do
response = request.post("/api/#{API::API.version}/internal")
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
it 'expects a POST LFS request to batch URL to be allowed' do
response = request.post('/root/rouge.git/info/lfs/objects/batch')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
end
end
context 'json requests to a read-only GitLab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } }
let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } }
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
end
it 'expects PATCH requests to be disallowed' do
response = request.patch('/test_request', content_json)
expect(response).to disallow_request_in_json
end
it 'expects PUT requests to be disallowed' do
response = request.put('/test_request', content_json)
expect(response).to disallow_request_in_json
end
it 'expects POST requests to be disallowed' do
response = request.post('/test_request', content_json)
expect(response).to disallow_request_in_json
end
it 'expects DELETE requests to be disallowed' do
response = request.delete('/test_request', content_json)
expect(response).to disallow_request_in_json
end
end
end
...@@ -39,6 +39,14 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do ...@@ -39,6 +39,14 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
it { is_expected.to eq(expected_result) } it { is_expected.to eq(expected_result) }
end end
it 'skips GitLab read-only instances' do
stub_user
stub_home_dir
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
is_expected.to be_truthy
end
end end
describe '#check?' do describe '#check?' do
......
...@@ -178,14 +178,13 @@ describe CacheMarkdownField do ...@@ -178,14 +178,13 @@ describe CacheMarkdownField do
end end
end end
describe '#refresh_markdown_cache!' do describe '#refresh_markdown_cache' do
before do before do
thing.foo = updated_markdown thing.foo = updated_markdown
end end
context 'do_update: false' do
it 'fills all html fields' do it 'fills all html fields' do
thing.refresh_markdown_cache! thing.refresh_markdown_cache
expect(thing.foo_html).to eq(updated_html) expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy expect(thing.foo_html_changed?).to be_truthy
...@@ -195,20 +194,24 @@ describe CacheMarkdownField do ...@@ -195,20 +194,24 @@ describe CacheMarkdownField do
it 'does not save the result' do it 'does not save the result' do
expect(thing).not_to receive(:update_columns) expect(thing).not_to receive(:update_columns)
thing.refresh_markdown_cache! thing.refresh_markdown_cache
end end
it 'updates the markdown cache version' do it 'updates the markdown cache version' do
thing.cached_markdown_version = nil thing.cached_markdown_version = nil
thing.refresh_markdown_cache! thing.refresh_markdown_cache
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end end
end end
context 'do_update: true' do describe '#refresh_markdown_cache!' do
before do
thing.foo = updated_markdown
end
it 'fills all html fields' do it 'fills all html fields' do
thing.refresh_markdown_cache!(do_update: true) thing.refresh_markdown_cache!
expect(thing.foo_html).to eq(updated_html) expect(thing.foo_html).to eq(updated_html)
expect(thing.foo_html_changed?).to be_truthy expect(thing.foo_html_changed?).to be_truthy
...@@ -219,7 +222,7 @@ describe CacheMarkdownField do ...@@ -219,7 +222,7 @@ describe CacheMarkdownField do
expect(thing).to receive(:persisted?).and_return(false) expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns) expect(thing).not_to receive(:update_columns)
thing.refresh_markdown_cache!(do_update: true) thing.refresh_markdown_cache!
end end
it 'saves the changes using #update_columns' do it 'saves the changes using #update_columns' do
...@@ -227,8 +230,7 @@ describe CacheMarkdownField do ...@@ -227,8 +230,7 @@ describe CacheMarkdownField do
expect(thing).to receive(:update_columns) expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION) .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
thing.refresh_markdown_cache!(do_update: true) thing.refresh_markdown_cache!
end
end end
end end
......
...@@ -12,6 +12,16 @@ describe Group, 'Routable' do ...@@ -12,6 +12,16 @@ describe Group, 'Routable' do
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) } it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end end
describe 'GitLab read-only instance' do
it 'does not save route if route is not present' do
group.route.path = ''
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect(group).to receive(:update_route).and_call_original
expect { group.full_path }.to change { Route.count }.by(0)
end
end
describe 'Callbacks' do describe 'Callbacks' do
it 'creates route record on create' do it 'creates route record on create' do
expect(group.route.path).to eq(group.path) expect(group.route.path).to eq(group.path)
......
...@@ -692,6 +692,44 @@ describe Project do ...@@ -692,6 +692,44 @@ describe Project do
project.cache_has_external_issue_tracker project.cache_has_external_issue_tracker
end.to change { project.has_external_issue_tracker}.to(false) end.to change { project.has_external_issue_tracker}.to(false)
end end
it 'does not cache data when in a read-only GitLab instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect do
project.cache_has_external_issue_tracker
end.not_to change { project.has_external_issue_tracker }
end
end
describe '#cache_has_external_wiki' do
let(:project) { create(:project, has_external_wiki: nil) }
it 'stores true if there is any external_wikis' do
services = double(:service, external_wikis: [ExternalWikiService.new])
expect(project).to receive(:services).and_return(services)
expect do
project.cache_has_external_wiki
end.to change { project.has_external_wiki}.to(true)
end
it 'stores false if there is no external_wikis' do
services = double(:service, external_wikis: [])
expect(project).to receive(:services).and_return(services)
expect do
project.cache_has_external_wiki
end.to change { project.has_external_wiki}.to(false)
end
it 'does not cache data when in a read-only GitLab instance' do
allow(Gitlab::Database).to receive(:read_only?) { true }
expect do
project.cache_has_external_wiki
end.not_to change { project.has_external_wiki }
end
end end
describe '#has_wiki?' do describe '#has_wiki?' do
...@@ -2446,7 +2484,7 @@ describe Project do ...@@ -2446,7 +2484,7 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_truthy expect(project.migrate_to_hashed_storage!).to be_truthy
end end
it 'flags as readonly' do it 'flags as read-only' do
expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true) expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true)
end end
...@@ -2573,7 +2611,7 @@ describe Project do ...@@ -2573,7 +2611,7 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_nil expect(project.migrate_to_hashed_storage!).to be_nil
end end
it 'does not flag as readonly' do it 'does not flag as read-only' do
expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only } expect { project.migrate_to_hashed_storage! }.not_to change { project.repository_read_only }
end end
end end
......
...@@ -824,6 +824,34 @@ describe 'Git LFS API and storage' do ...@@ -824,6 +824,34 @@ describe 'Git LFS API and storage' do
end end
end end
describe 'when handling lfs batch request on a read-only GitLab instance' do
let(:authorization) { authorize_user }
let(:project) { create(:project) }
let(:path) { "#{project.http_url_to_repo}/info/lfs/objects/batch" }
let(:body) do
{ 'objects' => [{ 'oid' => sample_oid, 'size' => sample_size }] }
end
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
project.team << [user, :master]
enable_lfs
end
it 'responds with a 200 message on download' do
post_lfs_json path, body.merge('operation' => 'download'), headers
expect(response).to have_gitlab_http_status(200)
end
it 'responds with a 403 message on upload' do
post_lfs_json path, body.merge('operation' => 'upload'), headers
expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => 'You cannot write to this read-only GitLab instance.')
end
end
describe 'when pushing a lfs object' do describe 'when pushing a lfs object' do
before do before do
enable_lfs enable_lfs
......
...@@ -20,7 +20,7 @@ describe Projects::HashedStorageMigrationService do ...@@ -20,7 +20,7 @@ describe Projects::HashedStorageMigrationService do
expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
end end
it 'updates project to be hashed and not readonly' do it 'updates project to be hashed and not read-only' do
service.execute service.execute
expect(project.hashed_storage?).to be_truthy expect(project.hashed_storage?).to be_truthy
......
...@@ -38,6 +38,18 @@ describe Users::ActivityService do ...@@ -38,6 +38,18 @@ describe Users::ActivityService do
end end
end end
end end
context 'when in GitLab read-only instance' do
before do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
it 'does not update last_activity_at' do
service.execute
expect(last_hour_user_ids).to eq([])
end
end
end end
def last_hour_user_ids def last_hour_user_ids
......
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