maestrano/mno-enterprise

View on GitHub
core/app/models/mno_enterprise/user.rb

Summary

Maintainability
A
1 hr
Test Coverage
# == Schema Information
#
# Endpoint:
#   - /v1/users
#   - /v1/organizations/:organization_id/users
#
#  id                             :string          e.g.: 1
#  uid                            :string          e.g.: usr-k3j23npo
#  email                          :string(255)     default(""), not null
#  authenticatable_salt           :string(255)     used for session authentication
#  encrypted_password             :string(255)     default(""), not null
#  reset_password_token           :string(255)
#  reset_password_sent_at         :datetime
#  remember_created_at            :datetime
#  sign_in_count                  :integer         default(0)
#  current_sign_in_at             :datetime
#  last_sign_in_at                :datetime
#  current_sign_in_ip             :string(255)
#  last_sign_in_ip                :string(255)
#  confirmation_token             :string(255)
#  confirmed_at                   :datetime
#  confirmation_sent_at           :datetime
#  unconfirmed_email              :string(255)
#  failed_attempts                :integer         default(0)
#  unlock_token                   :string(255)
#  locked_at                      :datetime
#  created_at                     :datetime        not null
#  updated_at                     :datetime        not null
#  name                           :string(255)
#  surname                        :string(255)
#  company                        :string(255)
#  phone                          :string(255)
#  phone_country_code             :string(255)
#  geo_country_code               :string(255)
#  geo_state_code                 :string(255)
#  geo_city                       :string(255)
#  website                        :string(255)
#  api_key                        :string(255)
#  api_secret                     :string(255)
#  mnoe_sub_tenant_id             :string
#

module MnoEnterprise
  class User < BaseResource
    include MnoEnterprise::Concerns::Models::IntercomUser if MnoEnterprise.intercom_enabled?
    extend Devise::Models

    # Note: password and encrypted_password are write-only attributes and are never returned by
    # the remote API. If you are looking for a session token, use authenticatable_salt
    attributes :uid, :email, :password, :current_password, :password_confirmation, :authenticatable_salt, :encrypted_password, :reset_password_token, :reset_password_sent_at,
      :remember_created_at, :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip,
      :last_sign_in_ip, :confirmation_token, :confirmed_at, :confirmation_sent_at, :unconfirmed_email,
      :failed_attempts, :unlock_token, :locked_at, :name, :surname, :company, :phone, :phone_country_code,
      :geo_country_code, :geo_state_code, :geo_city, :website, :orga_on_create, :sso_session, :current_password_required, :admin_role,
      :api_key, :api_secret, :developer, :kpi_enabled, :external_id, :meta_data, :password_valid, :mnoe_sub_tenant_id, :client_ids

    define_model_callbacks :validation #required by Devise

    devise_modules = [
      :remote_authenticatable, :registerable, :recoverable, :rememberable,
      :trackable, :validatable, :lockable, :confirmable, :timeoutable, :password_expirable,
      :omniauthable
    ]
    devise_modules.delete(:registerable) if Settings.try(:devise).try(:registration).try(:disabled)
    devise(*devise_modules, omniauth_providers: Devise.omniauth_providers)

    #================================
    # Validation
    #================================

    if Devise.password_regex
      validates :password, format: { with: Devise.password_regex, message: Devise.password_regex_message }, if: :password_required?
    end

    #================================
    # Associations
    #================================
    has_many :organizations, class_name: 'MnoEnterprise::Organization'
    has_many :org_invites, class_name: 'MnoEnterprise::OrgInvite'
    has_one :deletion_request, class_name: 'MnoEnterprise::DeletionRequest'
    has_many :teams, class_name: 'MnoEnterprise::Team'

    # Impac
    has_many :dashboards, class_name: 'MnoEnterprise::Impac::Dashboard'
    has_many :alerts, class_name: 'MnoEnterprise::Impac::Alert'

    has_many :clients, class_name: 'MnoEnterprise::Organization'

    #================================
    # Callbacks
    #================================
    before_save :expire_user_cache

    #================================
    # Class Methods
    #================================
    # The auth_hash includes an email and password
    # Return nil in case of failure
    def self.authenticate(auth_hash)
      # retro compatibilty issue: previous version were returning nil if the user was not found or if the password was invalid
      # see https://maestrano.atlassian.net/browse/MNOE-209
      # we now validate that the password is valid in /core/lib/devise/strategies/remote_authenticatable.rb
      u = self.post("user_sessions", auth_hash.merge(validate_password: true))
      if u && u.id
        u.clear_attribute_changes!
      end
      return u
    end


    #================================
    # Devise Confirmation
    # TODO: should go in a module
    #================================


    # Override Devise to allow confirmation via original token
    # Less secure but useful if user has been created by Maestrano Enterprise
    # (happens when an orga_invite is sent to a new user)
    #
    # Find a user by its confirmation token and try to confirm it.
    # If no user is found, returns a new user with an error.
    # If the user is already confirmed, create an error for the user
    # Options must have the confirmation_token
    def self.confirm_by_token(confirmation_token)
      confirmable = self.find_for_confirmation(confirmation_token)
      confirmable.perform_confirmation(confirmation_token)
      confirmable
    end

    # Find a user using a confirmation token
    def self.find_for_confirmation(confirmation_token)
      original_token     = confirmation_token
      confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)

      confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
      confirmable = find_or_initialize_with_error_by(:confirmation_token, original_token) if confirmable.errors.any?
      confirmable
    end

    # Confirm the user and store confirmation_token
    def perform_confirmation(confirmation_token)
      self.confirm if self.persisted?
      self.confirmation_token = confirmation_token
    end

    # It may happen that that the errors attribute become nil, which breaks the controller logic (rails responder)
    # This getter ensures that 'errors' is always initialized
    def errors
      @errors ||= ActiveModel::Errors.new(self)
    end

    # Don't require a password for unconfirmed users (user save password at confirmation step)
    def password_required?
      super if confirmed?
    end

    #================================
    # Instance Methods
    #================================

    def to_s
      "#{name} #{surname}"
    end

    # Format for audit log
    def to_audit_event
      {
        user_name: to_s,
        user_email: email
      }
    end

    # Default value for failed attempts
    def failed_attempts
      read_attribute(:failed_attempts) || 0
    end

    # Override Devise default method
    def authenticatable_salt
      read_attribute(:authenticatable_salt)
    end

    # Return the role of this user for the provided
    # organization
    def role(organization = nil)
      # Return cached version if available
      return self.read_attribute(:role) if !organization

      # Find in arrays if organizations have been fetched
      # already. Perform remote query otherwise
      org = begin
        if self.organizations.loaded?
          self.organizations.to_a.find { |e| e.id == organization.id }
        else
          self.organizations.where(id: organization.id).first
        end
      end

      org ? org.role : nil
    end

    def expire_user_cache
      Rails.cache.delete(['user', self.to_key])
      true # Don't skip save if above return false (memory_store)
    end

    def refresh_user_cache
      self.reload
      Rails.cache.write(['user', self.to_key], self)
    # singleton can't be dumped / undefined method `marshal_dump' for nil
    rescue TypeError, NoMethodError
      expire_user_cache
    end

    # Used by omniauth providers to find or create users
    # on maestrano
    # See Auth::OmniauthCallbacksController
    def self.find_for_oauth(auth, opts = {}, signed_in_resource = nil)
      # Get the identity and user if they exist
      identity = Identity.find_for_oauth(auth)

      # If a signed_in_resource is provided it always overrides the existing user
      # to prevent the identity being locked with accidentally created accounts.
      # Note that this may leave zombie accounts (with no associated identity) which
      # can be cleaned up at a later date.
      user = signed_in_resource ? signed_in_resource : identity.user

      # Create the user if needed
      if user.blank? # WTF is wrong with user.nil?
        # Get the existing user by email.
        email = auth.info.email
        user = self.where(email: email).first if email

        # Create the user if it's a new registration
        if user.nil?
          user = create_from_omniauth(auth, opts.except(:authorized_link_to_email))
        elsif auth.provider == 'intuit'
          unless opts[:authorized_link_to_email] == user.email
            # Intuit email is NOT a confirmed email. Therefore we need to ask the user to
            # login the old fashion to make sure it is the right user!
            fail(SecurityError, 'reconfirm credentials')
          end
        end
      end

      # Associate the identity with the user if needed
      if identity.user != user
        identity.user_id = user.id
        identity.save!
      end
      user
    end

    # Create a new user from omniauth hash
    def self.create_from_omniauth(auth, opts = {})
      user = User.new(
        name: auth.info.first_name.presence || auth.info.email[/(\S*)@/, 1],
        surname: auth.info.last_name.presence || '',
        email: auth.info.email,
        password: Devise.friendly_token[0, 20],
        avatar_url: auth.info.image.presence
      )

      # opts hash is expected to contain additional attributes
      # to set on the model
      user.assign_attributes(opts)

      # Skip email confirmation if not from Intuit (Intuit email is NOT a confirmed email)
      user.skip_confirmation! unless auth.provider == 'intuit'
      user.save!

      user
    end
  end
end