2021-08-05 08:09:57 -04:00
# 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-08-31 11:10:29 -04:00
# ::Gitlab::Database.allow_cross_joins_across_databases(url: link-to-issue-url)
2021-08-05 08:09:57 -04:00
#
# ...
# end
# end
module Database
module PreventCrossJoins
CrossJoinAcrossUnsupportedTablesError = Class . new ( StandardError )
2021-08-31 11:10:29 -04:00
ALLOW_THREAD_KEY = :allow_cross_joins_across_databases
2021-10-01 02:09:45 -04:00
ALLOW_ANNOTATE_KEY = ALLOW_THREAD_KEY . to_s . freeze
2021-08-31 11:10:29 -04:00
2021-08-05 08:09:57 -04:00
def self . validate_cross_joins! ( sql )
2021-10-01 02:09:45 -04:00
return if Thread . current [ ALLOW_THREAD_KEY ] || sql . include? ( ALLOW_ANNOTATE_KEY )
2021-08-05 08:09:57 -04:00
2021-08-25 23:09:01 -04:00
# 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-12-01 22:13:01 -05:00
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-08-31 02:08:50 -04:00
2021-10-27 11:13:41 -04:00
schemas = :: Gitlab :: Database :: GitlabSchema . table_schemas ( tables )
2021-08-05 08:09:57 -04:00
2021-08-25 23:09:01 -04:00
if schemas . include? ( :gitlab_ci ) && schemas . include? ( :gitlab_main )
Thread . current [ :has_cross_join_exception ] = true
2021-08-05 08:09:57 -04:00
raise CrossJoinAcrossUnsupportedTablesError ,
2021-09-27 23:11:11 -04:00
" Unsupported cross-join across ' #{ tables . join ( " , " ) } ' querying ' #{ schemas . to_a . join ( " , " ) } ' discovered " \
2021-09-07 02:11:06 -04:00
" 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-08-05 08:09:57 -04:00
end
end
2021-08-10 02:08:47 -04:00
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-08-31 11:10:29 -04:00
Thread . current [ ALLOW_THREAD_KEY ] = false
2021-08-10 02:08:47 -04:00
yield
ensure
ActiveSupport :: Notifications . unsubscribe ( subscriber ) if subscriber
end
2021-10-07 11:12:00 -04:00
def allow_cross_joins_across_databases ( url : , & block )
:: Gitlab :: Database . allow_cross_joins_across_databases ( url : url , & block )
end
2021-08-10 02:08:47 -04:00
end
2021-08-05 08:09:57 -04:00
module GitlabDatabaseMixin
def allow_cross_joins_across_databases ( url : )
2021-08-31 11:10:29 -04:00
old_value = Thread . current [ ALLOW_THREAD_KEY ]
Thread . current [ ALLOW_THREAD_KEY ] = true
yield
ensure
Thread . current [ ALLOW_THREAD_KEY ] = old_value
2021-08-05 08:09:57 -04:00
end
end
2021-10-01 02:09:45 -04:00
module ActiveRecordRelationMixin
def allow_cross_joins_across_databases ( url : )
super . annotate ( ALLOW_ANNOTATE_KEY )
end
end
2021-08-05 08:09:57 -04:00
end
end
Gitlab :: Database . singleton_class . prepend (
Database :: PreventCrossJoins :: GitlabDatabaseMixin )
2021-10-01 02:09:45 -04:00
ActiveRecord :: Relation . prepend (
Database :: PreventCrossJoins :: ActiveRecordRelationMixin )
2021-09-01 20:10:56 -04:00
ALLOW_LIST = Set . new ( YAML . load_file ( File . join ( __dir__ , 'cross-join-allowlist.yml' ) ) ) . freeze
2021-08-25 23:09:01 -04:00
2021-08-05 08:09:57 -04:00
RSpec . configure do | config |
2021-08-10 02:08:47 -04:00
config . include ( :: Database :: PreventCrossJoins :: SpecHelpers )
2021-08-31 02:08:50 -04:00
config . around do | example |
2021-08-25 23:09:01 -04:00
Thread . current [ :has_cross_join_exception ] = false
2021-10-22 02:10:16 -04:00
if ALLOW_LIST . include? ( example . file_path_rerun_argument )
2021-08-31 02:08:50 -04:00
example . run
else
with_cross_joins_prevented { example . run }
end
2021-08-05 08:09:57 -04:00
end
end