2021-10-27 15:23:28 +05:30
# frozen_string_literal: true
# This module tries to discover and prevent cross-joins across tables
# This will forbid usage of tables between CI and main database
# on a same query unless explicitly allowed by. This will change execution
# from a given point to allow cross-joins. The state will be cleared
# on a next test run.
#
# This method should be used to mark METHOD introducing cross-join
# not a test using the cross-join.
#
# class User
# def ci_owned_runners
2021-11-11 11:23:49 +05:30
# ::Gitlab::Database.allow_cross_joins_across_databases(url: link-to-issue-url)
2021-10-27 15:23:28 +05:30
#
# ...
# end
# end
module Database
module PreventCrossJoins
CrossJoinAcrossUnsupportedTablesError = Class . new ( StandardError )
2021-11-11 11:23:49 +05:30
ALLOW_THREAD_KEY = :allow_cross_joins_across_databases
2021-11-18 22:05:49 +05:30
ALLOW_ANNOTATE_KEY = ALLOW_THREAD_KEY . to_s . freeze
2023-04-23 21:23:45 +05:30
IGNORED_SCHEMAS = % i [ gitlab_shared gitlab_internal ] . freeze
2021-11-11 11:23:49 +05:30
2021-10-27 15:23:28 +05:30
def self . validate_cross_joins! ( sql )
2021-11-18 22:05:49 +05:30
return if Thread . current [ ALLOW_THREAD_KEY ] || sql . include? ( ALLOW_ANNOTATE_KEY )
2021-11-11 11:23:49 +05:30
# Allow spec/support/database_cleaner.rb queries to disable/enable triggers for many tables
# See https://gitlab.com/gitlab-org/gitlab/-/issues/339396
return if sql . include? ( " DISABLE TRIGGER " ) || sql . include? ( " ENABLE TRIGGER " )
2021-10-27 15:23:28 +05:30
2022-01-26 12:08:38 +05:30
tables = begin
PgQuery . parse ( sql ) . tables
rescue PgQuery :: ParseError
# PgQuery might fail in some cases due to limited nesting:
# https://github.com/pganalyze/pg_query/issues/209
return
end
2021-10-27 15:23:28 +05:30
2021-12-11 22:18:48 +05:30
schemas = :: Gitlab :: Database :: GitlabSchema . table_schemas ( tables )
2023-04-23 21:23:45 +05:30
schemas . subtract ( IGNORED_SCHEMAS )
2021-11-11 11:23:49 +05:30
2023-04-23 21:23:45 +05:30
if schemas . many?
2021-11-11 11:23:49 +05:30
Thread . current [ :has_cross_join_exception ] = true
2021-10-27 15:23:28 +05:30
raise CrossJoinAcrossUnsupportedTablesError ,
2021-11-18 22:05:49 +05:30
" Unsupported cross-join across ' #{ tables . join ( " , " ) } ' querying ' #{ schemas . to_a . join ( " , " ) } ' discovered " \
2021-11-11 11:23:49 +05:30
" when executing query ' #{ sql } '. Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html # removing-joins-between-ci_-and-non-ci_-tables for details on how to resolve this exception. "
2021-10-27 15:23:28 +05:30
end
end
module SpecHelpers
def with_cross_joins_prevented
subscriber = ActiveSupport :: Notifications . subscribe ( 'sql.active_record' ) do | event |
:: Database :: PreventCrossJoins . validate_cross_joins! ( event . payload [ :sql ] )
end
2021-11-11 11:23:49 +05:30
Thread . current [ ALLOW_THREAD_KEY ] = false
2021-10-27 15:23:28 +05:30
yield
ensure
ActiveSupport :: Notifications . unsubscribe ( subscriber ) if subscriber
end
2021-11-18 22:05:49 +05:30
def allow_cross_joins_across_databases ( url : , & block )
:: Gitlab :: Database . allow_cross_joins_across_databases ( url : url , & block )
end
2021-10-27 15:23:28 +05:30
end
module GitlabDatabaseMixin
def allow_cross_joins_across_databases ( url : )
2021-11-11 11:23:49 +05:30
old_value = Thread . current [ ALLOW_THREAD_KEY ]
Thread . current [ ALLOW_THREAD_KEY ] = true
yield
ensure
Thread . current [ ALLOW_THREAD_KEY ] = old_value
2021-10-27 15:23:28 +05:30
end
end
2021-11-18 22:05:49 +05:30
module ActiveRecordRelationMixin
def allow_cross_joins_across_databases ( url : )
super . annotate ( ALLOW_ANNOTATE_KEY )
end
end
2021-10-27 15:23:28 +05:30
end
end
Gitlab :: Database . singleton_class . prepend (
Database :: PreventCrossJoins :: GitlabDatabaseMixin )
2021-11-18 22:05:49 +05:30
ActiveRecord :: Relation . prepend (
Database :: PreventCrossJoins :: ActiveRecordRelationMixin )
2021-11-11 11:23:49 +05:30
ALLOW_LIST = Set . new ( YAML . load_file ( File . join ( __dir__ , 'cross-join-allowlist.yml' ) ) ) . freeze
2021-10-27 15:23:28 +05:30
RSpec . configure do | config |
config . include ( :: Database :: PreventCrossJoins :: SpecHelpers )
2021-11-11 11:23:49 +05:30
config . around do | example |
Thread . current [ :has_cross_join_exception ] = false
2021-12-11 22:18:48 +05:30
if ALLOW_LIST . include? ( example . file_path_rerun_argument )
2021-11-11 11:23:49 +05:30
example . run
else
with_cross_joins_prevented { example . run }
end
2021-10-27 15:23:28 +05:30
end
end