ManageIQ/manageiq

View on GitHub
app/models/miq_group.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
93%
class MiqGroup < ApplicationRecord
  USER_GROUP   = "user"
  SYSTEM_GROUP = "system"
  TENANT_GROUP = "tenant"

  belongs_to :tenant
  has_one    :entitlement, :dependent => :destroy, :autosave => true
  has_one    :miq_user_role, :through => :entitlement
  has_and_belongs_to_many :users
  has_many   :vms,         :dependent => :nullify
  has_many   :miq_templates, :dependent => :nullify
  has_many   :miq_reports, :dependent => :nullify
  has_many   :miq_report_results, :dependent => :nullify
  has_many   :miq_widget_contents, :dependent => :destroy
  has_many   :miq_widget_sets, :as => :owner, :dependent => :destroy
  has_many   :miq_product_features, :through => :miq_user_role
  has_many   :authentications, :dependent => :nullify

  virtual_delegate :miq_user_role_name, :to => :entitlement, :allow_nil => true, :type => :string
  virtual_column :read_only,          :type => :boolean
  virtual_has_one :sui_product_features, :class_name => "Array"

  delegate :self_service?, :limited_self_service?, :to => :miq_user_role, :allow_nil => true

  validates :description, :presence => true, :unique_within_region => {:match_case => false}
  validate :validate_default_tenant, :on => :update, :if => :tenant_id_changed?
  before_destroy :ensure_can_be_destroyed
  after_destroy :reset_current_group_for_users

  # For REST API compatibility only; Don't use otherwise!
  accepts_nested_attributes_for :entitlement

  serialize :settings

  default_value_for :group_type, USER_GROUP
  default_value_for(:sequence) { next_sequence }

  acts_as_miq_taggable
  include CustomAttributeMixin
  include ActiveVmAggregationMixin
  include TimezoneMixin
  include TenancyMixin
  include CustomActionsMixin
  include ExternalUrlMixin

  alias_method :current_tenant, :tenant

  def name
    description
  end

  def settings
    current = super
    return if current.nil?

    self.settings = current.with_indifferent_access
    super
  end

  def settings=(new_settings)
    indifferent_settings = new_settings.try(:with_indifferent_access)
    super(indifferent_settings)
  end

  def self.with_roles_excluding(identifier, allowed_ids: nil)
    scope = where.not(
      :id => MiqGroup
        .unscope(:select)
        .joins(:miq_product_features)
        .where(:miq_product_features => {:identifier => identifier})
        .select(:id)
    )
    scope = scope.or(where(:id => allowed_ids)) if allowed_ids.present?
    scope
  end

  def self.next_sequence
    # The +(current_scope || self)+ here is a result of a Rails 6.0 deprecation
    # warning that is added here:
    #
    #   https://github.com/rails/rails/pull/35280
    #
    # This was an attempt to fix "leaking scopes", however, in our case, we use
    # this method both for our +default_value_for(:sequence)+, and it will get
    # used as part of +.create_tenant_group+.
    #
    # As such, we need both +.current_scope+ for when we want to scope down the
    # +.next_sequence+, but also well allow it to be used on a raw +.create+
    # without scopes.
    #
    # Our +.current_scope+ use case can be best described via one our of
    # +MiqGroup+ specs:
    #
    #   expect(MiqGroup.next_sequence).to be < 999 # sanity check
    #
    #   FactoryBot.create(:miq_group, :description => "want 1", :sequence => 999)
    #   FactoryBot.create(:miq_group, :description => "want 2", :sequence => 1000)
    #   FactoryBot.create(:miq_group, :description => "dont want", :sequence => 1009)
    #
    #   expect(MiqGroup.where("description like 'want%'").next_sequence).to eq(1001)
    #
    #
    (current_scope || self).maximum(:sequence).to_i + 1
  end

  def self.seed
    role_map_file = FIXTURE_DIR.join("role_map.yaml")
    role_map = YAML.load_file(role_map_file) if role_map_file.exist?
    return unless role_map

    filter_map_file = FIXTURE_DIR.join("filter_map.yaml")
    ldap_to_filters = filter_map_file.exist? ? YAML.load_file(filter_map_file) : {}
    root_tenant = Tenant.root_tenant

    groups = where(:group_type => SYSTEM_GROUP, :tenant_id => Tenant.root_tenant)
               .includes(:entitlement).index_by(&:description)
    roles  = MiqUserRole.where("name like 'EvmRole-%'").index_by(&:name)

    role_map.each_with_index do |(group_name, role_name), index|
      group = groups[group_name] || new(:description => group_name)
      user_role = roles["EvmRole-#{role_name}"]
      if user_role.nil?
        raise StandardError,
              _("Unable to find user_role 'EvmRole-%{role_name}' for group '%{group_name}'") %
                {:role_name => role_name, :group_name => group_name}
      end
      group.miq_user_role       = user_role if group.entitlement.try(:miq_user_role_id) != user_role.id
      group.sequence            = index + 1
      group.entitlement.filters = ldap_to_filters[group_name]
      group.group_type          = SYSTEM_GROUP
      group.tenant              = root_tenant

      if group.changed?
        mode = group.new_record? ? "Created" : "Updated"
        group.save!
        _log.info("#{mode} Group: #{group.description} with Role: #{user_role.name}")
      end
    end

    # find any default tenant groups that do not have a role
    tenant_role = MiqUserRole.default_tenant_role
    if tenant_role
      tenant_groups.includes(:entitlement).where(:entitlements => {:miq_user_role_id => nil}).each do |group|
        if group.entitlement.present? # Relation is read-only if present
          Entitlement.update(group.entitlement.id, :miq_user_role => tenant_role)
        else
          group.update(:miq_user_role => tenant_role)
        end
      end
    else
      _log.warn("Unable to find default tenant role for tenant access")
    end
  end

  def self.strip_group_domains(group_list)
    group_list.collect { |group| group.gsub(/@.*/, '') }
  end

  def self.get_ldap_groups_by_user(user, bind_dn, bind_pwd)
    username = user.kind_of?(self) ? user.userid : user
    ldap = MiqLdap.new

    unless ldap.bind(ldap.fqusername(bind_dn), bind_pwd)
      raise _("Bind failed for user %{user_name}") % {:user_name => bind_dn}
    end

    user_obj = ldap.get_user_object(ldap.normalize(ldap.fqusername(username)))
    raise _("Unable to find user %{user_name} in directory") % {:user_name => username} if user_obj.nil?

    ldap.get_memberships(user_obj, ::Settings.authentication.group_memberships_max_depth)
  end

  def self.get_httpd_groups_by_user(user)
    if MiqEnvironment::Command.is_podified?
      get_httpd_groups_by_user_via_dbus_api_service(user)
    else
      get_httpd_groups_by_user_via_dbus(user)
    end
  end

  def self.get_httpd_groups_by_user_via_dbus(user)
    require "dbus"

    username = user.kind_of?(self) ? user.userid : user

    sysbus = DBus.system_bus
    ifp_service   = sysbus["org.freedesktop.sssd.infopipe"]
    ifp_object    = ifp_service.object("/org/freedesktop/sssd/infopipe")
    ifp_object.introspect
    ifp_interface = ifp_object["org.freedesktop.sssd.infopipe"]
    begin
      user_groups = ifp_interface.GetUserGroups(user)
    rescue => err
      raise _("Unable to get groups for user %{user_name} - %{error}") % {:user_name => username, :error => err}
    end
    strip_group_domains(user_groups.first)
  end

  def self.get_httpd_groups_by_user_via_dbus_api_service(user)
    require_dependency "httpd_dbus_api"

    groups = HttpdDBusApi.new.user_groups(user)
    strip_group_domains(groups)
  end

  def get_filters(type = nil)
    entitlement.try(:get_filters, type)
  end

  def has_filters?
    entitlement.try(:has_filters?) || false
  end

  def get_managed_filters
    entitlement.try(:get_managed_filters) || []
  end

  def get_belongsto_filters
    entitlement.try(:get_belongsto_filters) || []
  end

  def system_group?
    group_type == SYSTEM_GROUP
  end

  # @return true if this is a default tenant group
  def tenant_group?
    group_type == TENANT_GROUP
  end

  # Asks about the tenant's default_miq_group
  #
  # NOTE: this is the old definition for `tenant_group?`
  #
  # @return true if this is assigned to the tenant as the default tenant
  # @return false if the tenant is being deleted or pointing to a different group
  def referenced_by_tenant?
    tenant.try(:default_miq_group_id) == id
  end

  def read_only
    system_group? || tenant_group?
  end
  alias_method :read_only?, :read_only

  virtual_total :user_count, :users

  def description=(val)
    super(val.to_s.strip)
  end

  def ordered_widget_sets
    if settings && settings[:dashboard_order]
      MiqWidgetSet.where(:id => settings[:dashboard_order]).with_array_order(settings[:dashboard_order]).to_a
    else
      miq_widget_sets.sort_by { |a| a.name.downcase }
    end
  end

  def regional_groups
    self.class.regional_groups(self)
  end

  def self.regional_groups(group)
    where(arel_table.grouping(arel_table.lower(arel_table[:description]).eq(group.description.downcase)))
  end

  def self.create_tenant_group(tenant)
    tenant_full_name = (tenant.ancestors.map(&:name) + [tenant.name] + [tenant.id.to_s]).join("/")

    create_with(
      :description => "Tenant #{tenant_full_name} access"
    ).find_or_create_by!(
      :group_type => TENANT_GROUP,
      :tenant_id  => tenant.id
    )
  end

  def self.sort_by_desc
    all.sort_by { |g| g.description.downcase }
  end

  def self.tenant_groups
    where(:group_type => TENANT_GROUP)
  end

  def self.non_tenant_groups
    where.not(:group_type => TENANT_GROUP)
  end

  def self.non_tenant_groups_in_my_region
    in_my_region.non_tenant_groups
  end

  # parallel to User.with_groups - only show these groups
  def self.with_groups(miq_group_ids)
    where(:id => miq_group_ids)
  end

  def single_group_users?
    group_user_ids = user_ids
    return false if group_user_ids.empty?

    users.includes(:miq_groups).where(:id => group_user_ids).where.not(:miq_groups => {:id => id}).count != group_user_ids.size
  end

  def sui_product_features
    return [] unless miq_user_role.allows?(:identifier => 'sui')

    MiqProductFeature.feature_all_children('sui').each_with_object([]) do |sui_feature, sui_features|
      sui_features << sui_feature if miq_user_role.allows?(:identifier => sui_feature)
    end
  end

  def self.display_name(number = 1)
    n_('Group', 'Groups', number)
  end

  def delete_from_dashboard_order(widget_set_id)
    return if settings.blank?

    settings[:dashboard_order]&.delete(widget_set_id)
  end

  def add_to_dashboard_order(widget_set_id)
    self.settings ||= {:dashboard_order => []}
    self.settings[:dashboard_order] ||= []
    self.settings[:dashboard_order] |= [widget_set_id]
  end

  private

  # if this tenant is changing, make sure this is not a default group
  # NOTE: old tenant is Tenant.find(tenant_id_was)
  def validate_default_tenant
    if tenant_id_was && tenant_group?
      errors.add(:tenant_id, "cant change the tenant of a default group")
    end
  end

  def current_user_group?
    id == current_user_group.try(:id)
  end

  def ensure_can_be_destroyed
    errors.add(:base, _("The login group cannot be deleted")) if current_user_group?
    errors.add(:base, _("The group has users assigned that do not belong to any other group")) if single_group_users?
    errors.add(:base, _("A tenant default group can not be deleted")) if tenant_group? && referenced_by_tenant?
    errors.add(:base, _("A read only group cannot be deleted.")) if system_group?
    throw :abort unless errors[:base].empty?
  end

  # tell users that this group is goinga away - and the users should fix their current group
  def reset_current_group_for_users
    User.where(:current_group_id => id).each(&:change_current_group)
  end
end