-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3875 from 3scale/tenant_id
tenant_id related fixes
- Loading branch information
Showing
12 changed files
with
192 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# frozen_string_literal: true | ||
|
||
module ThreeScale | ||
class TenantIDIntegrityChecker | ||
attr_reader :tenant_id | ||
|
||
def initialize(attribute = :tenant_id) | ||
@attribute = attribute | ||
end | ||
|
||
def check | ||
processed = [] | ||
|
||
models_with_tenant_id.inject([]) do |inconsistent_found, model| | ||
Rails.logger.info "Tenant integrity of #{model}" | ||
inconsistent_found.concat associated_inconsistent_pairs(model, processed: processed) | ||
end | ||
end | ||
|
||
private | ||
|
||
def associated_inconsistent_pairs(model, processed: []) | ||
model.reflect_on_all_associations.inject([]) do |inconsistent_found, association| | ||
next inconsistent_found if can_skip_asociation?(association, processed: processed) | ||
|
||
processed << association | ||
|
||
inconsistent_found.concat inconsistent_pairs_for(model, association) | ||
end | ||
end | ||
|
||
def inconsistent_pairs_for(model, association) | ||
table = model.arel_table.name | ||
table_alias = last_table_alias_from_sql(model.joins(association.name).to_sql) | ||
found = model.joins(association.name).where.not("#{table_alias}.tenant_id = #{table}.tenant_id") | ||
found = found.merge(Account.where(provider: false).or(Account.where(provider: nil))) if model == Account && association.name == :provider_account | ||
found = pluck_pks(found, association: association, table: table, assoc_table: table_alias) | ||
found.map { ["#{model}#{_1}", association.name, "#{association.klass}#{_2}"] } | ||
end | ||
|
||
def pluck_pks(joined_relation, association:, assoc_table:, table:) | ||
model_pk = pk_fields association.active_record | ||
assoc_pk = pk_fields association.klass | ||
res = joined_relation.reorder('').pluck(*model_pk.map{"#{table}.#{_1}"}, *assoc_pk.map{"#{assoc_table}.#{_1}"}) | ||
res.map { [_1.slice(0, model_pk.size), _1.slice(model_pk.size..-1)] } | ||
end | ||
|
||
def pk_fields(model) | ||
# in oracle-enhanced model.connection.schema_cache.primary_keys returns nil for composite so can't use the cache | ||
model.primary_key ? Array(model.primary_key) : model.connection.primary_keys(model.table_name) | ||
end | ||
|
||
def can_skip_asociation?(association, processed: []) | ||
# we can ignore these as they can't be automatically excluded but are redundant for the check anyway | ||
ignored = { | ||
Service => %i[all_metrics], # all metrics of service and APIs used by service so is redundant | ||
Account => %i[provider_accounts], # only master has this and it is normal that all will mismatch | ||
} | ||
model = association.active_record | ||
|
||
return true if ignored[model]&.include?(association.name) | ||
|
||
# we live in a perfect world where all associations have an inverse so we can skip polymorphic ones | ||
return true if association.polymorphic? | ||
|
||
# arity can be one when association has a scope defined with a proc taking current object as argument | ||
# We can't handle such associations but we can ignore them if the inverse one we can handle | ||
if association.scope&.arity&.public_send(:>, 0) | ||
return true unless association.inverse_of.polymorphic? || association.inverse_of.scope&.arity&.public_send(:>, 0) | ||
raise "we can't handle #{association.name} of #{model}" | ||
end | ||
|
||
return true unless association.klass.attribute_names.include?("tenant_id") | ||
|
||
# skip indirect associations where the "through association" has tenant_id, because we will check that | ||
# indirect association through the "through association" later (or we did already) | ||
return true if association.through_reflection&.try(:klass)&.attribute_names&.include?("tenant_id") | ||
|
||
processed.any? {_1 == association || _1 == association.inverse_of } | ||
end | ||
|
||
def last_table_alias_from_sql(sql) | ||
matcher = /.*INNER JOIN [`'"]([\S]+)[`'"] (?:[`'"]([\S]+)[`'"] )?ON/i.match(sql) | ||
matcher[2] || matcher[1] | ||
end | ||
|
||
def models_with_tenant_id | ||
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models") | ||
all_models = ApplicationRecord.descendants.select(&:arel_table).reject(&:abstract_class?) | ||
all_models.select! { _1.attribute_names.include? "tenant_id" } | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'test_helper' | ||
|
||
module Tasks | ||
module Multitenant | ||
class TenantsIntegrityTest < ActiveSupport::TestCase | ||
test "reports tenant lack of integrity with belongs_to associations" do | ||
provider = FactoryBot.create(:simple_provider) | ||
wrong_buyers = FactoryBot.create_list(:simple_buyer, 2, provider_account: provider, tenant_id: provider.id + 1) | ||
FactoryBot.create(:simple_buyer, provider_account: provider, tenant_id: provider.id) | ||
|
||
expected_lines = ["Inconsistent tenant_ids for:"] | ||
expected_lines.concat(wrong_buyers.map { |buyer| "Account[#{buyer.id}] provider_account Account[#{provider.id}]" }) | ||
|
||
Rails.logger.expects(:error).with { |msg| expected_lines.all? { msg.include?(_1) } } | ||
|
||
execute_rake_task "multitenant/tenants.rake", "multitenant:tenants:integrity" | ||
end | ||
|
||
test "reports tenant lack of integrity with has_many associations" do | ||
plan = FactoryBot.create(:application_plan) | ||
wrong_cinstances = FactoryBot.create_list(:cinstance, 2, plan: plan, tenant_id: 0) | ||
FactoryBot.create(:cinstance, plan: plan) | ||
|
||
expected_lines = ["Inconsistent tenant_ids for:"] | ||
wrong_cinstances.each do |cinstance| | ||
expected_lines << "Account[#{cinstance.user_account.id}] bought_cinstances Cinstance[#{cinstance.id}]" | ||
expected_lines << "Account[#{cinstance.user_account.id}] contracts Contract[#{cinstance.id}]" | ||
expected_lines << "Contract[#{cinstance.id}] plan Plan[#{cinstance.plan.id}]" | ||
expected_lines << "Cinstance[#{cinstance.id}] plan ApplicationPlan[#{cinstance.plan.id}]" | ||
expected_lines << "Cinstance[#{cinstance.id}] service Service[#{cinstance.service.id}]" | ||
end | ||
|
||
Rails.logger.expects(:error).with { |msg| expected_lines.all? { msg.include?(_1) } } | ||
|
||
execute_rake_task "multitenant/tenants.rake", "multitenant:tenants:integrity" | ||
end | ||
|
||
test "reports tenant lack of integrity with complex primary keys" do | ||
plan = FactoryBot.create(:application_plan) | ||
feature = FactoryBot.create(:feature, featurable: plan.issuer, tenant_id: 0) | ||
plan.features << feature | ||
|
||
expected_lines = ["Inconsistent tenant_ids for:"] | ||
expected_lines << "Plan[#{plan.id}] features_plans FeaturesPlan[#{plan.id}, #{feature.id}]" | ||
expected_lines << "Service[#{plan.issuer.id}] features Feature[#{feature.id}]" | ||
|
||
Rails.logger.expects(:error).with { |msg| expected_lines.all? { msg.include?(_1) } } | ||
|
||
execute_rake_task "multitenant/tenants.rake", "multitenant:tenants:integrity" | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters