Merge branch 'tc-geo-read-only-idea' into 'master'
Create idea of read-only database Closes #37534 See merge request gitlab-org/gitlab-ce!14688
This commit is contained in:
commit
546b18b903
|
|
@ -3,9 +3,23 @@
|
|||
# Automatically sets the layout and ensures an administrator is logged in
|
||||
class Admin::ApplicationController < ApplicationController
|
||||
before_action :authenticate_admin!
|
||||
before_action :display_read_only_information
|
||||
layout 'admin'
|
||||
|
||||
def authenticate_admin!
|
||||
render_404 unless current_user.admin?
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ module Boards
|
|||
def index
|
||||
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
|
||||
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,
|
||||
:milestone,
|
||||
:assignees,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
|
|||
include LfsRequest
|
||||
|
||||
skip_before_action :lfs_check_access!, only: [:deprecated]
|
||||
before_action :lfs_check_batch_operation!, only: [:batch]
|
||||
|
||||
def batch
|
||||
unless objects.present?
|
||||
|
|
@ -90,4 +91,21 @@ class Projects::LfsApiController < Projects::GitHttpClientController
|
|||
}
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
|
|||
# Make sure merge requests created before 8.0
|
||||
# have head file in refs/merge-requests/
|
||||
def ensure_ref_fetched
|
||||
@merge_request.ensure_ref_fetched
|
||||
@merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
|
||||
end
|
||||
|
||||
def merge_request_params
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ class SessionsController < Devise::SessionsController
|
|||
prepend_before_action :check_initial_setup, only: [:new]
|
||||
prepend_before_action :authenticate_with_two_factor,
|
||||
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 :load_recaptcha
|
||||
|
||||
|
|
@ -86,28 +85,36 @@ class SessionsController < Devise::SessionsController
|
|||
end
|
||||
end
|
||||
|
||||
def store_redirect_path
|
||||
redirect_path =
|
||||
def stored_redirect_uri
|
||||
@redirect_to ||= stored_location_for(:redirect)
|
||||
end
|
||||
|
||||
def store_redirect_uri
|
||||
redirect_uri =
|
||||
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
|
||||
URI(request.referer)
|
||||
else
|
||||
request.fullpath
|
||||
URI(request.url)
|
||||
end
|
||||
|
||||
# Prevent a 'you are already signed in' message directly after signing:
|
||||
# we should never redirect to '/users/sign_in' after signing in successfully.
|
||||
unless URI(redirect_path).path == new_user_session_path
|
||||
store_location_for(:redirect, redirect_path)
|
||||
end
|
||||
return true if redirect_uri.path == new_user_session_path
|
||||
|
||||
redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
|
||||
|
||||
@redirect_to = redirect_to
|
||||
store_location_for(:redirect, redirect_to)
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def redirect_allowed_to?(uri)
|
||||
uri.host == Gitlab.config.gitlab.host &&
|
||||
uri.port == Gitlab.config.gitlab.port
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
find_user.try(:two_factor_enabled?)
|
||||
find_user&.two_factor_enabled?
|
||||
end
|
||||
|
||||
def auto_sign_in_with_provider
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ module CacheMarkdownField
|
|||
|
||||
# Update every column in a row if any one is invalidated, as we only store
|
||||
# one version per row
|
||||
def refresh_markdown_cache!(do_update: false)
|
||||
def refresh_markdown_cache
|
||||
options = { skip_project_check: skip_project_check? }
|
||||
|
||||
updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
|
||||
|
|
@ -71,8 +71,14 @@ module CacheMarkdownField
|
|||
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
|
||||
|
||||
updates.each {|html_field, data| write_attribute(html_field, data) }
|
||||
end
|
||||
|
||||
update_columns(updates) if persisted? && do_update
|
||||
def refresh_markdown_cache!
|
||||
updates = refresh_markdown_cache
|
||||
|
||||
return unless persisted? && Gitlab::Database.read_write?
|
||||
|
||||
update_columns(updates)
|
||||
end
|
||||
|
||||
def cached_html_up_to_date?(markdown_field)
|
||||
|
|
@ -124,8 +130,8 @@ module CacheMarkdownField
|
|||
end
|
||||
|
||||
# Using before_update here conflicts with elasticsearch-model somehow
|
||||
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
|
||||
before_update :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?
|
||||
end
|
||||
|
||||
class_methods do
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ module Routable
|
|||
end
|
||||
|
||||
def update_route
|
||||
return if Gitlab::Database.read_only?
|
||||
|
||||
prepare_route
|
||||
route.save
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,15 +43,17 @@ module TokenAuthenticatable
|
|||
write_attribute(token_field, token) if token
|
||||
end
|
||||
|
||||
# Returns a token, but only saves when the database is in read & write mode
|
||||
define_method("ensure_#{token_field}!") do
|
||||
send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
read_attribute(token_field)
|
||||
end
|
||||
|
||||
# Resets the token, but only saves when the database is in read & write mode
|
||||
define_method("reset_#{token_field}!") do
|
||||
write_new_token(token_field)
|
||||
save!
|
||||
save! if Gitlab::Database.read_write?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def check_if_can_be_merged
|
||||
return unless unchecked?
|
||||
return unless unchecked? && Gitlab::Database.read_write?
|
||||
|
||||
can_be_merged =
|
||||
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
|
||||
|
|
|
|||
|
|
@ -824,7 +824,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def has_wiki?
|
||||
|
|
@ -844,7 +844,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def find_or_initialize_services(exceptions: [])
|
||||
|
|
|
|||
|
|
@ -459,6 +459,14 @@ class User < ActiveRecord::Base
|
|||
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
|
||||
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!
|
||||
transaction do
|
||||
update_attributes(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ module Keys
|
|||
end
|
||||
|
||||
def update?
|
||||
return false if ::Gitlab::Database.read_only?
|
||||
|
||||
last_used = key.last_used_at
|
||||
|
||||
return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Users
|
|||
private
|
||||
|
||||
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})")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Create idea of read-only database
|
||||
merge_request: 14688
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -154,6 +154,9 @@ module Gitlab
|
|||
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
|
||||
ENV['GIT_TERMINAL_PROMPT'] = '0'
|
||||
|
||||
# Gitlab Read-only middleware support
|
||||
config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
|
||||
|
||||
config.generators do |g|
|
||||
g.factory_girl false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,3 +24,15 @@ else
|
|||
run_query
|
||||
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
|
|||
return cacheless_render_field(object, field)
|
||||
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)
|
||||
end
|
||||
|
|
@ -162,10 +162,5 @@ module Banzai
|
|||
return unless cache_key
|
||||
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
# GitLab EE needs to disable updates on GET requests in Geo
|
||||
def self.update_object?(object)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ module Gitlab
|
|||
adapter_name.casecmp('postgresql').zero?
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def self.read_only?
|
||||
false
|
||||
end
|
||||
|
||||
def self.read_write?
|
||||
!self.read_only?
|
||||
end
|
||||
|
||||
def self.version
|
||||
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ module Gitlab
|
|||
command_not_allowed: "The command you're trying to execute 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.',
|
||||
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
|
||||
|
||||
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
|
||||
|
|
@ -161,7 +162,11 @@ module Gitlab
|
|||
|
||||
def check_push_access!(changes)
|
||||
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
|
||||
|
||||
if deploy_key
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
module Gitlab
|
||||
class GitAccessWiki < GitAccess
|
||||
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."
|
||||
}.freeze
|
||||
|
||||
|
|
@ -17,6 +18,10 @@ module Gitlab
|
|||
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
|
||||
end
|
||||
|
||||
if Gitlab::Database.read_only?
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
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
|
|||
].freeze
|
||||
|
||||
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?
|
||||
!home_dir || !File.directory?(home_dir)
|
||||
Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
|
||||
end
|
||||
|
||||
def check?
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ FactoryGirl.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :readonly do
|
||||
trait :read_only do
|
||||
repository_read_only true
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,14 @@ describe Banzai::Renderer do
|
|||
let(:object) { fake_object(fresh: false) }
|
||||
|
||||
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')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -598,6 +598,19 @@ describe Gitlab::GitAccess do
|
|||
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
|
||||
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
|
||||
|
||||
describe 'build authentication abilities' do
|
||||
|
|
@ -632,6 +645,16 @@ describe Gitlab::GitAccess do
|
|||
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
|
||||
let(:key) { create(:deploy_key, user: user, can_push: can_push) }
|
||||
let(:actor) { key }
|
||||
|
|
|
|||
|
|
@ -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(:project) { create(:project, :repository) }
|
||||
let(:user) { create(:user) }
|
||||
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
|
||||
let(:redirected_path) { nil }
|
||||
let(:authentication_abilities) do
|
||||
[
|
||||
|
|
@ -13,19 +14,27 @@ describe Gitlab::GitAccessWiki do
|
|||
]
|
||||
end
|
||||
|
||||
describe 'push_allowed?' do
|
||||
before do
|
||||
create(:protected_branch, name: 'master', project: project)
|
||||
project.team << [user, :developer]
|
||||
describe '#push_access_check' do
|
||||
context 'when user can :create_wiki' do
|
||||
before do
|
||||
create(:protected_branch, name: 'master', project: project)
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
subject { access.check('git-receive-pack', changes) }
|
||||
|
||||
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
|
||||
|
||||
it 'does not give access to upload wiki code' do
|
||||
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "You can't push code to a read-only GitLab instance.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
subject { access.check('git-receive-pack', changes) }
|
||||
|
||||
it { expect { subject }.not_to raise_error }
|
||||
end
|
||||
|
||||
def changes
|
||||
['6f6d7e7ed 570e7b2ab refs/heads/master']
|
||||
end
|
||||
|
||||
describe '#access_check_download!' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
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
|
|||
|
||||
it { is_expected.to eq(expected_result) }
|
||||
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
|
||||
|
||||
describe '#check?' do
|
||||
|
|
|
|||
|
|
@ -178,57 +178,59 @@ describe CacheMarkdownField do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#refresh_markdown_cache' do
|
||||
before do
|
||||
thing.foo = updated_markdown
|
||||
end
|
||||
|
||||
it 'fills all html fields' do
|
||||
thing.refresh_markdown_cache
|
||||
|
||||
expect(thing.foo_html).to eq(updated_html)
|
||||
expect(thing.foo_html_changed?).to be_truthy
|
||||
expect(thing.baz_html_changed?).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not save the result' do
|
||||
expect(thing).not_to receive(:update_columns)
|
||||
|
||||
thing.refresh_markdown_cache
|
||||
end
|
||||
|
||||
it 'updates the markdown cache version' do
|
||||
thing.cached_markdown_version = nil
|
||||
thing.refresh_markdown_cache
|
||||
|
||||
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refresh_markdown_cache!' do
|
||||
before do
|
||||
thing.foo = updated_markdown
|
||||
end
|
||||
|
||||
context 'do_update: false' do
|
||||
it 'fills all html fields' do
|
||||
thing.refresh_markdown_cache!
|
||||
it 'fills all html fields' do
|
||||
thing.refresh_markdown_cache!
|
||||
|
||||
expect(thing.foo_html).to eq(updated_html)
|
||||
expect(thing.foo_html_changed?).to be_truthy
|
||||
expect(thing.baz_html_changed?).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not save the result' do
|
||||
expect(thing).not_to receive(:update_columns)
|
||||
|
||||
thing.refresh_markdown_cache!
|
||||
end
|
||||
|
||||
it 'updates the markdown cache version' do
|
||||
thing.cached_markdown_version = nil
|
||||
thing.refresh_markdown_cache!
|
||||
|
||||
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
|
||||
end
|
||||
expect(thing.foo_html).to eq(updated_html)
|
||||
expect(thing.foo_html_changed?).to be_truthy
|
||||
expect(thing.baz_html_changed?).to be_truthy
|
||||
end
|
||||
|
||||
context 'do_update: true' do
|
||||
it 'fills all html fields' do
|
||||
thing.refresh_markdown_cache!(do_update: true)
|
||||
it 'skips saving if not persisted' do
|
||||
expect(thing).to receive(:persisted?).and_return(false)
|
||||
expect(thing).not_to receive(:update_columns)
|
||||
|
||||
expect(thing.foo_html).to eq(updated_html)
|
||||
expect(thing.foo_html_changed?).to be_truthy
|
||||
expect(thing.baz_html_changed?).to be_truthy
|
||||
end
|
||||
thing.refresh_markdown_cache!
|
||||
end
|
||||
|
||||
it 'skips saving if not persisted' do
|
||||
expect(thing).to receive(:persisted?).and_return(false)
|
||||
expect(thing).not_to receive(:update_columns)
|
||||
it 'saves the changes using #update_columns' do
|
||||
expect(thing).to receive(:persisted?).and_return(true)
|
||||
expect(thing).to receive(:update_columns)
|
||||
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
|
||||
|
||||
thing.refresh_markdown_cache!(do_update: true)
|
||||
end
|
||||
|
||||
it 'saves the changes using #update_columns' do
|
||||
expect(thing).to receive(:persisted?).and_return(true)
|
||||
expect(thing).to receive(:update_columns)
|
||||
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
|
||||
|
||||
thing.refresh_markdown_cache!(do_update: true)
|
||||
end
|
||||
thing.refresh_markdown_cache!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,16 @@ describe Group, 'Routable' do
|
|||
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
|
||||
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
|
||||
it 'creates route record on create' do
|
||||
expect(group.route.path).to eq(group.path)
|
||||
|
|
|
|||
|
|
@ -693,6 +693,44 @@ describe Project do
|
|||
project.cache_has_external_issue_tracker
|
||||
end.to change { project.has_external_issue_tracker}.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_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
|
||||
|
||||
describe '#has_wiki?' do
|
||||
|
|
@ -2500,7 +2538,7 @@ describe Project do
|
|||
expect(project.migrate_to_hashed_storage!).to be_truthy
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
@ -2627,7 +2665,7 @@ describe Project do
|
|||
expect(project.migrate_to_hashed_storage!).to be_nil
|
||||
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 }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -825,6 +825,34 @@ describe 'Git LFS API and storage' do
|
|||
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
|
||||
before do
|
||||
enable_lfs
|
||||
|
|
|
|||
|
|
@ -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
|
||||
end
|
||||
|
||||
it 'updates project to be hashed and not readonly' do
|
||||
it 'updates project to be hashed and not read-only' do
|
||||
service.execute
|
||||
|
||||
expect(project.hashed_storage?).to be_truthy
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ describe Users::ActivityService do
|
|||
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
|
||||
|
||||
def last_hour_user_ids
|
||||
|
|
|
|||
Loading…
Reference in New Issue