ManageIQ/manageiq

View on GitHub
app/models/tenant.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
require 'ancestry'

class Tenant < ApplicationRecord
  HARDCODED_LOGO = "custom_logo.png"
  HARDCODED_LOGIN_LOGO = "custom_login_logo.png"
  DEFAULT_URL = nil

  include ActiveVmAggregationMixin
  include CustomActionsMixin
  include TenantQuotasMixin
  include ExternalUrlMixin

  acts_as_miq_taggable

  default_value_for :name,        "My Company"
  default_value_for :description, "Tenant for My Company"
  default_value_for :divisible,   true
  default_value_for :use_config_for_attributes, false

  before_destroy :ensure_can_be_destroyed

  has_ancestry(:orphan_strategy => :restrict)

  has_many :providers
  has_many :ext_management_systems
  has_many :vm_or_templates
  has_many :vms, :inverse_of => :tenant
  has_many :miq_templates, :inverse_of => :tenant
  has_many :service_template_catalogs
  has_many :service_templates

  has_many :tenant_quotas
  has_many :miq_groups
  has_many :users, :through => :miq_groups
  has_many :ae_domains, :dependent => :destroy, :class_name => 'MiqAeDomain'
  has_many :miq_requests, :dependent => :destroy
  has_many :miq_request_tasks, :dependent => :destroy
  has_many :services, :dependent => :destroy
  has_many :shares
  has_many :authentications, :dependent => :nullify
  has_many :miq_product_features, :dependent => :destroy
  has_many :service_template_tenants, :dependent => :destroy
  has_many :service_templates, :through => :service_template_tenants

  belongs_to :default_miq_group, :class_name => "MiqGroup", :dependent => :destroy
  belongs_to :source, :polymorphic => true

  validates :subdomain, :uniqueness_when_changed => true, :allow_nil => true
  validates :domain,    :uniqueness_when_changed => true, :allow_nil => true
  validate  :validate_only_one_root
  validates :description, :presence => true
  validates :name, :presence                  => true, :unless => :use_config_for_attributes?,
                   :uniqueness_when_changed   => {:scope      => :ancestry,
                                                  :conditions => -> { in_my_region },
                                                  :message    => "should be unique per parent"}
  validate :validate_default_tenant, :on => :update, :if => :saved_change_to_default_miq_group_id?

  scope :all_tenants,  -> { where(:divisible => true) }
  scope :all_projects, -> { where(:divisible => false) }

  virtual_column :parent_name,  :type => :string
  virtual_column :display_type, :type => :string

  before_save :nil_blanks
  after_save -> { MiqProductFeature.invalidate_caches }
  after_create :create_tenant_group, :create_miq_product_features_for_tenant_nodes, :update_miq_product_features_for_tenant_nodes

  def self.scope_by_tenant?
    true
  end

  def self.with_current_tenant
    current_tenant = User.current_user.current_tenant
    where(:id => current_tenant.id)
  end

  def self.tenant_id_clause(user_or_group)
    strategy = Rbac.accessible_tenant_ids_strategy(self)
    tenant = user_or_group.try(:current_tenant)
    return [] if tenant.root?

    tenant_ids = tenant.accessible_tenant_ids(strategy)

    return if tenant_ids.empty?

    {table_name => {:id => tenant_ids}}
  end

  def all_subtenants
    self.class.descendants_of(self).where(:divisible => true)
  end

  def all_subprojects
    self.class.descendants_of(self).where(:divisible => false)
  end

  def regional_tenants
    self.class.regional_tenants(self)
  end

  def nested_service_templates
    ServiceTemplate.with_tenant(id)
  end

  def nested_providers
    ExtManagementSystem.with_tenant(id)
  end

  def nested_ae_namespaces
    MiqAeDomain.with_tenant(id)
  end

  def self.regional_tenants(tenant)
    where(arel_table.grouping(arel_table.lower(arel_table[:name]).eq(tenant.name.downcase)))
  end

  def accessible_tenant_ids(strategy = nil)
    (strategy ? regional_tenants.map(&strategy.to_sym).flatten : []) + regional_tenants.ids
  end

  def name
    tenant_attribute(:name, :company)
  end

  def parent_name
    parent.try(:name)
  end

  def display_type
    project? ? "Project" : "Tenant"
  end

  def login_text
    tenant_attribute(:login_text, :custom_login_text)
  end

  def get_quotas
    tenant_quotas.each_with_object({}) do |q, h|
      h[q.name.to_sym] = q.quota_hash
    end.reverse_merge(TenantQuota.quota_definitions)
  end

  def set_quotas(quotas)
    updated_keys = []

    self.class.transaction do
      quotas.each do |name, values|
        next if values[:value].nil?

        name = name.to_s
        q = tenant_quotas.detect { |tq| tq.name == name } || tenant_quotas.build(:name => name)
        q.update!(values)
        updated_keys << name
      end
      # Delete any quotas that were not passed in
      tenant_quotas.destroy_missing(updated_keys)
      # unfortunately, an extra scope is created in destroy_missing, so we need to reload the records
      tenant_quotas.reload
    end

    get_quotas
  end

  def used_quotas
    tenant_quotas.each_with_object({}) do |q, h|
      h[q.name.to_sym] = q.quota_hash.merge(:value => q.used)
    end.reverse_merge(TenantQuota.quota_definitions)
  end

  # Amount of quotas allocated to the immediate child tenants
  def allocated_quotas
    tenant_quotas.each_with_object({}) do |q, h|
      h[q.name.to_sym] = q.quota_hash.merge(:value => q.allocated)
    end.reverse_merge(TenantQuota.quota_definitions)
  end

  # Amount of quotas available to be allocated to child tenants
  def available_quotas
    tenant_quotas.each_with_object({}) do |q, h|
      h[q.name.to_sym] = q.quota_hash.merge(:value => q.available)
    end.reverse_merge(TenantQuota.quota_definitions)
  end

  def combined_quotas
    TenantQuota.quota_definitions.each_with_object({}) do |d, h|
      scope_name, _ = d
      q = tenant_quotas.send(scope_name).take || tenant_quotas.build(:name => scope_name, :value => 0)
      h[q.name.to_sym] = q.quota_hash
      h[q.name.to_sym][:allocated]   = q.allocated
      h[q.name.to_sym][:available]   = q.available unless q.new_record?
      h[q.name.to_sym][:used]        = q.used
    end.reverse_merge(TenantQuota.quota_definitions)
  end

  # @return [Boolean] Is this a default tenant?
  def default?
    root?
  end

  # @return [Boolean] Is this the root tenant?
  def root?
    !parent_id?
  end

  def tenant?
    divisible?
  end

  def project?
    !divisible?
  end

  def visible_domains
    MiqAeDomain.where(:tenant_id => path_ids).joins(:tenant).order('tenants.ancestry DESC NULLS LAST, priority DESC')
  end

  def enabled_domains
    visible_domains.where(:enabled => true)
  end

  def editable_domains
    ae_domains.where(:source => MiqAeDomain::USER_SOURCE).order('priority DESC')
  end

  def sequenceable_domains
    ae_domains.where.not(:source => MiqAeDomain::SYSTEM_SOURCE).order('priority DESC')
  end

  def any_editable_domains?
    ae_domains.where(:source => MiqAeDomain::USER_SOURCE).count > 0
  end

  def reset_domain_priority_by_ordered_ids(ids)
    uneditable_domains = visible_domains - editable_domains
    uneditable_domains.delete_if { |domain| domain.name == MiqAeDatastore::MANAGEIQ_DOMAIN }
    MiqAeDomain.reset_priority_by_ordered_ids(uneditable_domains.collect(&:id).reverse + ids)
  end

  # The default tenant is the tenant to be used when
  # the url does not map to a known domain or subdomain
  #
  # At this time, urls are not used, so the root tenant is returned
  # @return [Tenant] default tenant
  def self.default_tenant
    root_tenant
  end

  # the root tenant is also referred to as tenant0
  # from this tenant, all tenants are positioned
  #
  # @return [Tenant] the root tenant
  def self.root_tenant
    @root_tenant ||= root_tenant_without_cache
  end

  def self.root_tenant_without_cache
    in_my_region.roots.first
  end

  # NOTE: returns the root tenant
  def self.seed
    root_tenant || create!(:use_config_for_attributes => true) do |_|
      _log.info("Creating root tenant")
    end
  end

  # tenant
  #   tenant2
  #     project4 (!divisible)
  #   tenant3
  # @return [Array(Array<Array(String, Numeric)>, Array<Array(String, Numeric)>) ] tenants and projects
  #   e.g.:
  #   [
  #     [["tenant", 1], ["tenant/tenant2", 2]], ["tenant/tenant3", 3]]
  #     [["tenant/tenant2/project4", 4]]
  #   ]
  def self.tenant_and_project_names
    all_tenants_and_projects = Tenant.in_my_region.select(:id, :ancestry, :divisible, :use_config_for_attributes, :name)
    tenants_by_id = all_tenants_and_projects.index_by(&:id)

    tenants_and_projects = Rbac.filtered(Tenant.in_my_region.select(:id, :ancestry, :divisible, :use_config_for_attributes, :name))
                           .to_a.sort_by { |t| [t.ancestry || "", t.name] }

    tenants_and_projects.partition(&:divisible?).map do |tenants|
      tenants.map do |t|
        all_names = (t.ancestor_ids + [t.id]).map { |tid| tenants_by_id[tid] }.map(&:name)
        [all_names.join("/"), t.id]
      end.sort_by(&:first)
    end
  end

  #   Tenant
  #      Tenant A
  #      Tenant B
  #
  # @return [Array(JSON({name => String, id => Numeric, parent => Numeric}))] all subtenants of a tenant
  # e.g.:
  #   [
  #     {name=>"Tenant A",id=>2,parent=>1},
  #     {name=>"Tenant B",id=>3,parent=>1}
  #   ]
  def build_tenant_tree
    data_tenant = []
    all_subtenants.each do |subtenant|
      next unless subtenant.parent_name == name

      data_tenant.push(:name => subtenant.name, :id => subtenant.id, :parent => id)
      if subtenant.all_subtenants.count > 0
        data_tenant.concat(subtenant.build_tenant_tree)
      end
    end
    data_tenant
  end

  def allowed?
    Rbac::Filterer.filtered_object(self).present?
  end

  def create_miq_product_features_for_tenant_nodes
    MiqProductFeature.seed_single_tenant_miq_product_features(self)
  end

  def update_miq_product_features_for_tenant_nodes
    MiqProductFeature.invalidate_caches_queue
  end

  def destroy_with_subtree
    subtree.sort_by(&:depth).reverse.each(&:destroy)
  end

  private

  # when a root tenant has an attribute with a nil value,
  #   read the value from the configurations table instead
  #
  # @return the attribute value
  def tenant_attribute(attr_name, setting_name)
    if use_config_for_attributes?
      ret = ::Settings.server[setting_name]
      block_given? ? yield(ret) : ret
    else
      self[attr_name]
    end
  end

  def nil_blanks
    self.subdomain = nil unless subdomain.present?
    self.domain = nil unless domain.present?

    self.name = nil unless name.present?
  end

  # validates that there is only one tree
  def validate_only_one_root
    unless parent_id || parent
      root = self.class.root_tenant_without_cache
      errors.add(:parent, "required") if root && root != self
    end
  end

  def create_tenant_group
    update!(:default_miq_group => MiqGroup.create_tenant_group(self)) unless default_miq_group_id
    self
  end

  def ensure_can_be_destroyed
    errors.add(:base, _("A tenant with groups associated cannot be deleted.")) if miq_groups.non_tenant_groups.exists?
    errors.add(:base, _("A tenant created by tenant mapping cannot be deleted.")) if source&.persisted?
    throw :abort unless errors[:base].empty?
  end

  def validate_default_tenant
    if default_miq_group.tenant_id != id || !default_miq_group.tenant_group?
      errors.add(:default_miq_group, "default group must be a default group for this tenant")
    end
  end
end