hackedteam/rcs-db

View on GitHub
lib/rcs-db/db_objects/user.rb

Summary

Maintainability
A
25 mins
Test Coverage
require 'mongoid'
require 'bcrypt'

class User
  include RCS::Tracer
  include Mongoid::Document
  include Mongoid::Timestamps

  PRIVS = ['ADMIN',
            'ADMIN_USERS',
            'ADMIN_OPERATIONS',
            'ADMIN_TARGETS',
            'ADMIN_AUDIT',
            'ADMIN_LICENSE',
           'SYS',
            'SYS_FRONTEND',
            'SYS_BACKEND',
            'SYS_BACKUP',
            'SYS_INJECTORS',
            'SYS_CONNECTORS',
           'TECH',
            'TECH_FACTORIES',
            'TECH_BUILD',
            'TECH_CONFIG',
            'TECH_EXEC',
            'TECH_UPLOAD',
            'TECH_IMPORT',
            'TECH_NI_RULES',
           'VIEW',
            'VIEW_ALERTS',
            'VIEW_FILESYSTEM',
            'VIEW_EDIT',
            'VIEW_DELETE',
            'VIEW_EXPORT',
            'VIEW_PROFILES'
          ]

  field :name, type: String
  field :pass, type: String
  field :desc, type: String
  field :contact, type: String
  field :privs, type: Array
  field :enabled, type: Boolean
  field :locale, type: String
  field :timezone, type: Integer
  field :dashboard_ids, type: Array, default: []
  field :recent_ids, type: Array, default: []
  field :pwd_changed_at, type: DateTime
  field :pwd_changed_cs, type: String

  has_and_belongs_to_many :groups, :dependent => :nullify, :autosave => true
  has_many :alerts, :dependent => :destroy
  has_one :session, :dependent => :destroy, :autosave => true

  index({name: 1}, {background: true, unique: true})
  index({enabled: 1}, {background: true})

  store_in collection: 'users'

  before_destroy :destroy_sessions

  # Runs only if dashboard_ids has been updated
  after_save { rebuild_watched_items if changed_attributes['dashboard_ids'] }

  scope :enabled, where(enabled: true)

  scope :online, lambda {
    online_user_id = Session.only(:user_id).map(&:user_id)
    enabled.in(_id: online_user_id)
  }

  # Password must be 10 characters long and contains at least 1 number, 1 uppercase letter and 1 downcase letter
  STRONG_PWD_REGEXP = /(?=.*[a-z]+)(?=.*[A-Z]+)(?=.*[0-9]+)(?=.{10,})/

  validates_uniqueness_of :name, if: :name_changed?, message: "USER_ALREADY_EXISTS"

  validates_format_of :pass, with: STRONG_PWD_REGEXP, if: :password_changed?, message: "WEAK_PASSWORD"

  # Password must not contains the username
  validate do
    errors.add(:pass, "WEAK_PASSWORD") if password_changed? and password_match_username?
  end

  before_save do
    if password_changed? and !is_pass_hashed?
      self.pass = hash_password(self.pass)
      reset_pwd_changed_at
    end
  end

  def is_pass_hashed?
    !!(BCrypt::Password.new(self.pass) rescue false)
  end

  def calculate_pwd_changed_cs
    Digest::MD5.hexdigest("#{self.id}_#{self.pwd_changed_at.to_i}")
  end

  def reset_pwd_changed_at(datetime = now)
    self.pwd_changed_at = datetime
    self.pwd_changed_cs = calculate_pwd_changed_cs
  end

  def password_match_username?
    return false unless self.pass
    return false unless self.name

    self.pass.downcase =~ /#{self.name}/i
  end

  def rebuild_watched_items
    WatchedItem.rebuild
  end

  def hash_password(password)
    BCrypt::Password.create(password).to_s
  end

  def password_expired?
    return false if password_never_expire?

    # Someone tried to change pwd_changed_at via mongodb
    return true if pwd_changed_cs != calculate_pwd_changed_cs

    password_days_left.zero?
  end

  def pwd_changed_at
    datetime = attributes[:pwd_changed_at] || attributes['pwd_changed_at']
    datetime.utc if datetime
  end

  def password_expiring?
    password_days_left <= 15
  end

  def password_days_left
    return Float::INFINITY if password_never_expire?

    three_months = 30 * 3
    elapsed_days = ((now - pwd_changed_at) / (3600 * 24)).round
    remaining_days = three_months - elapsed_days
    remaining_days < 0 ? 0 : remaining_days
  end

  def password_never_expire?
    if pwd_changed_at.nil? and pwd_changed_cs.nil? # user not migrated
      true
    else
      !!RCS::DB::Config.instance.global['PASSWORDS_NEVER_EXPIRE']
    end
  end

  def now
    Time.now.utc
  end

  def password_changed?
    return true if new_record?
    changed_attributes.keys.include?('pass')
  end

  def name_changed?
    return true if new_record?
    changed_attributes.keys.include?('name')
  end

  def has_password?(password)
    BCrypt::Password.new(self.pass) == password
  end

  def add_recent(item)
    self.recent_ids.insert(0, {'section' => item[:section], 'type' => item[:type], 'id' => item[:id]})
    self.recent_ids.uniq!
    self.recent_ids = self.recent_ids[0..4]
    self.save
  rescue Exception => ex
    msg = "cannot add #{item[:type]} #{item[:id]} to recent list of user #{self.id}: #{ex.message}" rescue ex.message
    trace :error, "#add_recent, #{msg}"
  end

  def delete_item(id)
    if self.dashboard_ids.include? id
      trace :debug, "Deleting Item #{id} from #{self.name} dashboard"
      self.dashboard_ids.delete(id)
      self.save
    end

    self.recent_ids.each do |recent|
      # next unless recent['id'] == id or recent[:id] == id
      if recent == id or (recent.respond_to?(:[]) and (recent['id'] == id or recent[:id] == id))
        trace :debug, "Deleting Item #{id} from #{self.name} recents"
        self.recent_ids.delete(recent)
        self.save
      end
    end
  end

  def destroy_sessions
    ::Session.destroy_all(user: [self._id])
  end
end