Skip to content
Snippets Groups Projects
Commit d1366971 authored by Toon Claes's avatar Toon Claes
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 127 additions and 34 deletions
Loading
Loading
@@ -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
Loading
Loading
@@ -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,
Loading
Loading
Loading
Loading
@@ -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?
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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|
Loading
Loading
@@ -71,8 +71,14 @@ module CacheMarkdownField
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
 
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
 
def cached_html_up_to_date?(markdown_field)
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -156,6 +156,8 @@ module Routable
end
 
def update_route
return if Gitlab::Database.read_only?
prepare_route
route.save
end
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -814,7 +814,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?
Loading
Loading
@@ -834,7 +834,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: [])
Loading
Loading
Loading
Loading
@@ -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(
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
---
title: Create idea of read-only database
merge_request: 14688
author:
type: changed
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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`).
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
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
 
Loading
Loading
@@ -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
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment