openSUSE/open-build-service

View on GitHub
src/api/app/models/user.rb

Summary

Maintainability
D
2 days
Test Coverage
A
95%
require 'kconv'
require 'api_error'

class User < ApplicationRecord
  include CanRenderModel
  include Flipper::Identifier

  # Keep in sync with states defined in db/schema.rb
  STATES = %w[unconfirmed confirmed locked deleted subaccount].freeze
  NOBODY_LOGIN = '_nobody_'.freeze
  MAX_BIOGRAPHY_LENGTH_ALLOWED = 250

  enum :color_theme, { 'system' => 0, 'light' => 1, 'dark' => 2 }

  # disable validations because there can be users which don't have a bcrypt
  # password yet. this is for backwards compatibility
  has_secure_password validations: false

  has_many :watched_items, dependent: :destroy
  has_many :groups_users, inverse_of: :user
  has_many :roles_users, inverse_of: :user
  has_many :relationships, inverse_of: :user, dependent: :destroy

  has_many :comments, dependent: :destroy, inverse_of: :user
  has_many :status_messages
  has_many :tokens, class_name: 'Token', dependent: :destroy, inverse_of: :executor, foreign_key: :executor_id

  has_and_belongs_to_many :shared_workflow_tokens,
                          class_name: 'Token::Workflow',
                          join_table: :workflow_token_users,
                          association_foreign_key: :token_id,
                          dependent: :destroy,
                          inverse_of: :token_workflow

  has_many :reviews, dependent: :nullify

  has_many :event_subscriptions, inverse_of: :user

  belongs_to :owner, class_name: 'User', optional: true
  has_many :subaccounts, class_name: 'User', foreign_key: 'owner_id'

  has_many :requests_created, foreign_key: 'creator', primary_key: :login, class_name: 'BsRequest'

  # users have a n:m relation to group
  has_and_belongs_to_many :groups, -> { distinct }
  # users have a n:m relation to roles
  has_and_belongs_to_many :roles, -> { distinct }

  has_many :bs_request_actions_seen_by_users, dependent: :nullify
  has_many :bs_request_actions_seen, through: :bs_request_actions_seen_by_users, source: :bs_request_action

  has_one :ec2_configuration, class_name: 'Cloud::Ec2::Configuration', dependent: :destroy
  has_one :azure_configuration, class_name: 'Cloud::Azure::Configuration', dependent: :destroy
  has_many :upload_jobs, class_name: 'Cloud::User::UploadJob', dependent: :destroy

  has_many :notifications, -> { order(created_at: :desc) }, as: :subscriber, dependent: :destroy

  has_many :commit_activities

  has_many :status_message_acknowledgements, dependent: :destroy
  has_many :acknowledged_status_messages, through: :status_message_acknowledgements, class_name: 'StatusMessage', source: 'status_message'

  has_many :disabled_beta_features, dependent: :destroy
  has_many :reports, as: :reportable, dependent: :nullify
  has_many :submitted_reports, class_name: 'Report'

  has_many :moderated_comments, class_name: 'Comment', foreign_key: 'moderator_id'
  has_many :decisions, foreign_key: 'moderator_id'
  has_many :canned_responses, dependent: :destroy
  has_many :blocked_users, foreign_key: 'blocker_id', dependent: :destroy

  scope :confirmed, -> { where(state: 'confirmed') }
  scope :all_without_nobody, -> { where.not(login: NOBODY_LOGIN) }
  scope :not_deleted, -> { where.not(state: 'deleted') }
  scope :not_locked, -> { where.not(state: 'locked') }
  scope :active, -> { confirmed.or(User.unscoped.where(state: :subaccount, owner: User.unscoped.confirmed)) }
  scope :staff, -> { joins(:roles).where('roles.title' => 'Staff') }
  scope :admins, -> { joins(:roles).where('roles.title' => 'Admin') }
  scope :moderators, -> { joins(:roles).where('roles.title' => 'Moderator') }

  scope :in_beta, -> { where(in_beta: true) }
  scope :in_rollout, -> { where(in_rollout: true) }

  scope :list, lambda {
    all_without_nobody.includes(:owner).select(:id, :login, :email, :state, :realname, :owner_id, :updated_at, :ignore_auth_services)
  }

  scope :with_email, -> { where.not(email: [nil, '']) }

  scope :seen_since, ->(since) { where('last_logged_in_at > ?', since) }

  validates :login, :state, presence: { message: 'must be given' }

  validates :login,
            uniqueness: { case_sensitive: true, message: 'is the name of an already existing user' }

  validates :login,
            format: { with: /\A[\w $\^\-.#*+&'"]*\z/,
                      message: 'must not contain invalid characters' }
  validates :login,
            length: { in: 2..100, allow_nil: true,
                      too_long: 'must have less than 100 characters',
                      too_short: 'must have more than two characters' }

  validates :state, inclusion: { in: STATES }

  validate :validate_state

  # We want a valid email address. Note that the checking done here is very
  # rough. Email adresses are hard to validate now domain names may include
  # language specific characters and user names can be about anything anyway.
  # However, this is not *so* bad since users have to answer on their email
  # to confirm their registration.
  validates :email,
            format: { with: /\A([\w\-.\#$%&!?*'+=(){}|~]+)@([0-9a-zA-Z\-.\#$%&!?*'=(){}|~]+)+\z/,
                      message: 'must be a valid email address',
                      allow_blank: true }

  # we disabled has_secure_password's validations. therefore we need to do manual validations
  validate :password_validation
  validates :password, length: { minimum: 6, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED }, allow_nil: true
  validates :password, confirmation: true, allow_blank: true
  validates :biography, length: { maximum: MAX_BIOGRAPHY_LENGTH_ALLOWED }
  validates :rss_secret, uniqueness: true, length: { maximum: 200 }, allow_blank: true
  validates :color_theme, inclusion: { in: color_themes.keys }, if: -> { Flipper.enabled?('color_themes') }

  after_create :create_home_project, :measure_create
  after_update :measure_delete

  def create_home_project
    # avoid errors during seeding
    return if login.in?([NOBODY_LOGIN, 'Admin'])
    # may be disabled via Configuration setting
    return unless can_create_project?(home_project_name)

    # find or create the project
    project = Project.find_by(name: home_project_name)
    return if project

    project = Project.create!(name: home_project_name)
    project.commit_user = self
    # make the user maintainer
    project.relationships.create!(user: self, role: Role.find_by_title('maintainer'))
    project.store
    @home_project = project
  end

  # Inform ActiveModel::Dirty that changes are persistent now
  after_save :changes_applied

  # When a record object is initialized, we set the state and the login failure
  # count to unconfirmed/0 when it has not been set yet.
  before_validation(on: :create) do
    self.state ||= 'unconfirmed'

    # Set the last login time etc. when the record is created at first.
    self.last_logged_in_at = Time.zone.today

    self.login_failure_count = 0 if login_failure_count.nil?
  end

  def self.autocomplete_token(prefix = '')
    autocomplete_login(prefix).collect { |user| { name: user } }
  end

  def self.autocomplete_login(prefix = '')
    AutocompleteFinder::User.new(User.not_deleted.not_locked, prefix).call.pluck(:login)
  end

  # the default state of a user based on the api configuration
  def self.default_user_state
    ::Configuration.registration == 'confirmation' ? 'unconfirmed' : 'confirmed'
  end

  def self.create_user_with_fake_pw!(attributes = {})
    create!(attributes.merge(password: SecureRandom.base64(48)))
  end

  # This static method tries to find a user with the given login and password
  # in the database. Returns the user or nil if they could not be found
  def self.find_with_credentials(login, password)
    if CONFIG['ldap_mode'] == :on
      UserLdapStrategy.find_with_credentials(login, password)
    else
      find_by(login: login)&.authenticate_via_password(password)
    end
  end

  # Currently logged in user or nobody user if there is no user logged in.
  # Use this to check permissions, but don't treat it as logged in user. Check
  # is_nobody? on the returned object
  def self.possibly_nobody
    current || nobody
  end

  # Currently logged in user. Will throw an exception if no user is logged in.
  # So the controller needs to require login if using this (or models using it)
  def self.session!
    raise ArgumentError, 'Requiring user, but found nobody' unless session

    current
  end

  # Currently logged in user or nil
  def self.session
    current if current && !current.is_nobody?
  end

  def self.admin_session?
    current && current.is_admin?
  end

  # set the user as current session user (should be real user)
  def self.session=(user)
    Thread.current[:user] = user
  end

  def self.default_admin
    admin = CONFIG['default_admin'] || 'Admin'
    user = User.find_by!(login: admin)
    raise NotFoundError, "Admin not found, user #{admin} has not admin permissions" unless user.is_admin?

    user
  end

  def self.find_nobody!
    User.create_with(email: 'nobody@localhost',
                     realname: 'Anonymous User',
                     state: 'locked',
                     password: '123456').find_or_create_by(login: NOBODY_LOGIN)
  end

  def self.find_by_login!(login)
    user = not_deleted.find_by(login: login)
    return user if user

    raise NotFoundError, "Couldn't find User with login = #{login}"
  end

  # some users have last_logged_in_at empty
  def last_logged_in_at
    self[:last_logged_in_at] || created_at
  end

  def away?
    last_logged_in_at < 3.months.ago
  end

  def authenticate_via_password(password)
    if authenticate(password)
      mark_login!
      self
    else
      count_login_failure
      nil
    end
  end

  def validate_state
    # check that the state transition is valid
    errors.add(:state, 'must be a valid new state from the current state') unless state_transition_allowed?(state_was, state)
  end

  # This method checks whether the given value equals the password when
  # hashed with this user's password hash type. Returns a boolean.
  def deprecated_password_equals?(value)
    hash_string(value) == deprecated_password
  end

  def authenticate(unencrypted_password)
    # for users without a bcrypt password we need an extra check and convert
    # the password to a bcrypt one
    if deprecated_password
      if deprecated_password_equals?(unencrypted_password)
        update(password: unencrypted_password, deprecated_password: nil, deprecated_password_salt: nil, deprecated_password_hash_type: nil)
        return self
      end

      return false
    end

    # it seems that the user is not using a deprecated password so we use bcrypt's
    # #authenticate method
    super
  end

  # Returns true if the the state transition from "from" state to "to" state
  # is valid. Returns false otherwise.
  #
  # Note that currently no permission checking is included here; It does not
  # matter what permissions the currently logged in user has, only that the
  # state transition is legal in principle.
  def state_transition_allowed?(from, to)
    from = from.to_i
    to = to.to_i

    return true if from == to # allow keeping state

    case from
    when 'unconfirmed'
      true
    when 'confirmed'
      to.in?(%w[locked deleted])
    when 'locked'
      to.in?(%w[confirmed deleted])
    when 'deleted'
      to == 'confirmed'
    else
      false
    end
  end

  def cloud_configurations?
    ec2_configuration.present? || azure_configuration.present?
  end

  def to_axml(_opts = {})
    render_axml
  end

  def render_axml(watchlist: false, render_watchlist_only: false)
    # CanRenderModel
    render_xml(watchlist: watchlist, render_watchlist_only: render_watchlist_only)
  end

  def home_project_name
    "home:#{login}"
  end

  def home_project
    @home_project ||= Project.find_by(name: home_project_name)
  end

  def branch_project_name(branch)
    "#{home_project_name}:branches:#{branch}"
  end

  #####################
  # permission checks #
  #####################

  def is_admin?
    return @is_admin unless @is_admin.nil?

    @is_admin = roles.exists?(title: 'Admin')
  end

  def is_staff?
    return @is_staff unless @is_staff.nil?

    @is_staff = roles.exists?(title: 'Staff')
  end

  def is_nobody?
    login == NOBODY_LOGIN
  end

  def is_moderator?
    roles.exists?(title: 'Moderator')
  end

  def is_active?
    return owner.is_active? if owner

    self.state == 'confirmed'
  end

  def is_deleted?
    state == 'deleted'
  end

  def list_groups
    lookup_strategy.list_groups(self)
  end

  def is_in_group?(group)
    case group
    when String
      group = Group.find_by_title(group)
    when Integer
      group = Group.find(group)
    when Group, nil
      nil
    else
      raise ArgumentError, "illegal parameter type to User#is_in_group?: #{group.class}"
    end

    group && lookup_strategy.is_in_group?(self, group)
  end

  # This method returns true if the user is granted the permission with one
  # of the given permission titles.
  def has_global_permission?(perm_string)
    logger.debug "has_global_permission? #{perm_string}"
    roles.detect do |role|
      return true if role.static_permissions.find_by(title: perm_string)
    end
  end

  # FIXME: This should be a policy
  def can_modify?(object, ignore_lock = nil)
    case object
    when Project
      can_modify_project?(object, ignore_lock)
    when Package
      can_modify_package?(object, ignore_lock)
    when nil
      false
    else
      raise ArgumentError, "Wrong type of object: '#{object.class}' instead of Project or Package."
    end
  end

  # FIXME: This should be a policy
  # project is instance of Project
  def can_modify_project?(project, ignore_lock = nil)
    raise ArgumentError, "illegal parameter type to User#can_modify_project?: #{project.class.name}" unless project.is_a?(Project)

    if project.new_record?
      # Project.check_write_access(!) should have been used?
      raise NotFoundError, 'Project is not stored yet'
    end

    can_modify_project_internal(project, ignore_lock)
  end

  # FIXME: This should be a policy
  # package is instance of Package
  def can_modify_package?(package, ignore_lock = nil)
    return false if package.nil? # happens with remote packages easily
    raise ArgumentError, "illegal parameter type to User#can_modify_package?: #{package.class.name}" unless package.is_a?(Package)
    return false if !ignore_lock && package.is_locked?
    return true if is_admin?
    return true if has_global_permission?('change_package')
    return true if has_local_permission?('change_package', package)

    false
  end

  # FIXME: This should be a policy
  # project_name is name of the project
  def can_create_project?(project_name)
    ## special handling for home projects
    return true if project_name == home_project_name && Configuration.allow_user_to_create_home_project
    return true if /^#{home_project_name}:/ =~ project_name && Configuration.allow_user_to_create_home_project

    return true if has_global_permission?('create_project')

    parent_project = Project.new(name: project_name).parent
    return false if parent_project.nil?
    return true  if is_admin?

    has_local_permission?('create_project', parent_project)
  end

  # FIXME: This should be a policy
  def can_modify_attribute_definition?(object)
    can_create_attribute_definition?(object)
  end

  def attribute_modifier_rule_matches?(rule)
    return false if rule.user && rule.user != self
    return false if rule.group && !is_in_group?(rule.group)

    true
  end

  # FIXME: This should be a policy
  def can_create_attribute_definition?(object)
    object = object.attrib_namespace if object.is_a?(AttribType)
    raise ArgumentError, "illegal parameter type to User#can_change?: #{object.class.name}" unless object.is_a?(AttribNamespace)

    return true if is_admin?

    abies = object.attrib_namespace_modifiable_bies.includes(%i[user group])
    abies.any? { |rule| attribute_modifier_rule_matches?(rule) }
  end

  def attribute_modification_rule_matches?(rule, object)
    return false unless attribute_modifier_rule_matches?(rule)
    return false if rule.role && !has_local_role?(rule.role, object)

    true
  end

  # FIXME: This should be a policy
  def can_create_attribute_in?(object, atype)
    raise ArgumentError, "illegal parameter type to User#can_change?: #{object.class.name}" if !object.is_a?(Project) && !object.is_a?(Package)

    return true if is_admin?

    abies = atype.attrib_type_modifiable_bies.includes(%i[user group role])
    # no rules -> maintainer
    return can_modify?(object) if abies.empty?

    abies.any? { |rule| attribute_modification_rule_matches?(rule, object) }
  end

  # FIXME: This should be a policy
  def can_download_binaries?(package)
    can?(:download_binaries, package)
  end

  # FIXME: This should be a policy
  def can_source_access?(package)
    can?(:source_access, package)
  end

  # FIXME: This should be a policy
  def can?(key, package)
    is_admin? ||
      has_global_permission?(key.to_s) ||
      has_local_permission?(key.to_s, package)
  end

  def has_local_role?(role, object)
    if object.is_a?(Package) || object.is_a?(Project)
      logger.debug "running local role package check: user #{login}, package #{object.name}, role '#{role.title}'"
      rels = object.relationships.where(role_id: role.id, user_id: id)
      return true if rels.exists?

      rels = object.relationships.joins(:groups_users).where(groups_users: { user_id: id }).where(role_id: role.id)
      return true if rels.exists?

      return true if lookup_strategy.local_role_check(role, object)
    end

    return has_local_role?(role, object.project) if object.is_a?(Package)

    false
  end

  # local permission check
  # if context is a package, check permissions in package, then if needed continue with project check
  # if context is a project, check it, then if needed go down through all namespaces until hitting the root
  # return false if none of the checks succeed
  # rubocop:disable Metrics/PerceivedComplexity
  def has_local_permission?(perm_string, object)
    roles = Role.ids_with_permission(perm_string)
    return false unless roles

    parent = nil
    case object
    when Package
      logger.debug "running local permission check: user #{login}, package #{object.name}, permission '#{perm_string}'"
      # check permission for given package
      parent = object.project

      # Users have permissions to manage packages in their own home project
      # This is needed since users sometimes remove themselves from the maintainers of their own home project
      return true if parent.name == home_project_name
    when Project
      logger.debug "running local permission check: user #{login}, project #{object.name}, permission '#{perm_string}'"

      # Users have permissions to manage their own home project and it's subprojects
      # This is needed since users sometimes remove themselves from the maintainers of their own home project
      return true if object.name == home_project_name || object.name.starts_with?("#{home_project_name}:")

      # check permission for given project
      parent = object.parent
    when nil
      return has_global_permission?(perm_string)
    else
      return false
    end
    rel = object.relationships.where(user_id: id).where(role_id: roles)
    return true if rel.exists?

    rel = object.relationships.joins(:groups_users).where(groups_users: { user_id: id }).where(role_id: roles)
    return true if rel.exists?

    return true if lookup_strategy.local_permission_check(roles, object)

    if parent
      # check permission of parent project
      logger.debug "permission not found, trying parent project '#{parent.name}'"
      return has_local_permission?(perm_string, parent)
    end

    false
  end
  # rubocop:enable Metrics/PerceivedComplexity

  def lock!
    self.state = 'locked'
    save!

    # lock also all home projects to avoid unneccessary builds
    Project.where('name like ?', "#{home_project_name}%").find_each do |prj|
      next if prj.is_locked?

      prj.lock('User account got locked')
    end
  end

  def delete
    delete!
  rescue ActiveRecord::RecordInvalid
    false
  end

  def delete!(adminnote: nil)
    # remove user data as much as possible
    # but we must NOT remove the information that the account did exist
    # or another user could take over the identity which can open security
    # issues (other infrastructur and systems using repositories)

    self.adminnote = adminnote if adminnote.present?
    self.email = ''
    self.realname = ''
    self.state = 'deleted'
    comments.destroy_all
    event_subscriptions.destroy_all
    save!

    # wipe also all home projects
    destroy_home_projects(reason: 'User account got deleted')

    true
  end

  def destroy_home_projects(reason:)
    Project.where('name LIKE ?', "#{home_project_name}:%").or(Project.where(name: home_project_name)).find_each do |project|
      project.commit_opts = { comment: reason.to_s }
      project.destroy
    end
  end

  def involved_projects
    Project.unscoped.for_user(id).or(Project.unscoped.for_group(group_ids))
  end

  # lists packages maintained by this user and are not in maintained projects
  def involved_packages
    Package.unscoped.for_user(id).or(Package.unscoped.for_group(group_ids)).where.not(project: involved_projects)
  end

  # lists reviews involving this user
  def involved_reviews(search = nil)
    result = BsRequest.by_user_reviews(id).or(
      BsRequest.by_project_reviews(involved_projects).or(
        BsRequest.by_package_reviews(involved_packages).or(
          BsRequest.by_group_reviews(groups)
        )
      )
    ).with_actions_and_reviews.where(state: :review, reviews: { state: :new }).where.not(creator: login)
    search.present? ? result.do_search(search) : result
  end

  # list requests involving this user
  def declined_requests(search = nil)
    result = requests_created.where(state: :declined).with_actions
    search.present? ? result.do_search(search) : result
  end

  # list incoming requests involving this user
  def incoming_requests(search = nil, states: [:new])
    result = BsRequest.where(state: states).and(
      BsRequest.where(id: BsRequestAction.bs_request_ids_of_involved_projects(involved_projects.pluck(:id))).or(
        BsRequest.where(id: BsRequestAction.bs_request_ids_of_involved_packages(involved_packages.pluck(:id)))
      )
    ).with_actions

    search.present? ? result.do_search(search) : result
  end

  # list outgoing requests involving this user
  def outgoing_requests(search = nil, states: %i[new review])
    result = requests_created.where(state: states).with_actions
    search.present? ? result.do_search(search) : result
  end

  # list of all requests
  def requests(search = nil)
    project_ids = involved_projects.pluck(:id)
    package_ids = involved_packages.pluck(:id)

    actions = BsRequestAction.bs_request_ids_of_involved_projects(project_ids).or(
      BsRequestAction.bs_request_ids_of_involved_packages(package_ids)
    )

    reviews = Review.bs_request_ids_of_involved_users(id).or(
      Review.bs_request_ids_of_involved_projects(project_ids).or(
        Review.bs_request_ids_of_involved_packages(package_ids).or(
          Review.bs_request_ids_of_involved_groups(groups)
        )
      )
    ).where(state: :new)

    result = BsRequest.where(creator: login).or(
      BsRequest.where(id: actions).or(
        BsRequest.where(id: reviews)
      )
    ).with_actions

    search.present? ? result.do_search(search) : result
  end

  # TODO: This should be in a query object
  def involved_patchinfos
    @involved_patchinfos ||= Package.joins(:issues).includes({ issues: :issue_tracker }, :package_kinds, :project)
                                    .where(issues: { state: 'OPEN', owner_id: id },
                                           package_kinds: { kind: 'patchinfo' })
                                    .distinct
  end

  def user_relevant_packages_for_status
    MaintainedPackagesByUserFinder.new(self).call.pluck(:id)
  end

  def state
    return owner.state if owner

    self[:state]
  end

  def to_s
    login
  end

  def to_param
    to_s
  end

  def tasks
    Rails.cache.fetch("requests_for_#{cache_key_with_version}") do
      declined_requests.count +
        incoming_requests.count +
        involved_reviews.count
    end
  end

  def unread_notifications_count
    notifications.for_web.unread.size
  end

  def update_globalroles(global_roles)
    roles.replace(global_roles + roles.where(global: false))
  end

  def add_globalrole(global_role)
    update_globalroles(global_role + roles.global)
  end

  def display_name
    address = Mail::Address.new(email)
    address.display_name = realname
    address.format
  end

  def name
    realname.presence || login
  end

  def combined_rss_feed_items
    Notification.for_rss.where(subscriber: self).or(
      Notification.for_rss.where(subscriber: groups)
    ).order(created_at: :desc, id: :desc).limit(Notification::MAX_RSS_ITEMS_PER_USER)
  end

  def mark_login!
    update(last_logged_in_at: Time.zone.today, login_failure_count: 0)
  end

  def count_login_failure
    update(login_failure_count: login_failure_count + 1)
  end

  def proxy_realname(env)
    return unless env['HTTP_X_FIRSTNAME'].present? && env['HTTP_X_LASTNAME'].present?

    "#{env['HTTP_X_FIRSTNAME'].force_encoding('UTF-8')} #{env['HTTP_X_LASTNAME'].force_encoding('UTF-8')}"
  end

  def update_login_values(env)
    # updates user's email and real name using data transmitted by authentication proxy
    self.email = env['HTTP_X_EMAIL'] if env['HTTP_X_EMAIL'].present?
    self.realname = proxy_realname(env) if proxy_realname(env)

    self.last_logged_in_at = Time.zone.today
    self.login_failure_count = 0

    if changes.any?
      logger.info "updating email for user #{login} from proxy header: old:#{email}|new:#{env['HTTP_X_EMAIL']}" if changes.key?('email')

      # At this point some login value changed, so a successful log in is tracked
      RabbitmqBus.send_to_bus('metrics', 'login,access_point=webui value=1')
    end

    save
  end

  def run_as
    before = User.session
    begin
      User.session = self
      yield
    ensure
      User.session = before
    end
  end

  def watched_requests
    BsRequest.where(id: watched_items.where(watchable_type: 'BsRequest').pluck(:watchable_id)).order('number DESC')
  end

  def watched_packages
    Package.where(id: watched_items.where(watchable_type: 'Package').pluck(:watchable_id)).order('LOWER(name), name')
  end

  def watched_projects
    Project.where(id: watched_items.where(watchable_type: 'Project').pluck(:watchable_id)).order('LOWER(name), name')
  end

  # Can't use ActiveRecord::SecureToken because we don't want User to have
  # a rss_secret by default. We want to skip creating Notification for the
  # RSS channel if people don't use it.
  def regenerate_rss_secret
    update!(rss_secret: SecureRandom.base58(24))
  end

  private

  def measure_create
    RabbitmqBus.send_to_bus('metrics', 'user.create value=1')
  end

  def measure_delete
    return unless saved_change_to_attribute?('state', to: 'deleted')

    RabbitmqBus.send_to_bus('metrics', 'user.delete value=1')
  end

  # The currently logged in user (might be nil). It's reset after
  # every request and normally set during authentification
  def self.current
    Thread.current[:user]
  end
  private_class_method :current

  def self.nobody
    Thread.current[:nobody] ||= find_nobody!
  end
  private_class_method :nobody

  def password_validation
    return if password_digest || deprecated_password

    errors.add(:password, 'can\'t be blank')
  end

  # FIXME: This should be a policy
  def can_modify_project_internal(project, ignore_lock)
    # The ordering is important because of the lock status check
    return false if !ignore_lock && project.is_locked?
    return true if is_admin?

    return true if has_global_permission?('change_project')
    return true if has_local_permission?('change_project', project)

    false
  end

  # Hashes the given parameter by the selected hashing method. It uses the
  # "password_salt" property's value to make the hashing more secure.
  def hash_string(value)
    crypt2index = { 'md5crypt' => 1,
                    'sha256crypt' => 5 }
    if deprecated_password_hash_type == 'md5'
      Digest::MD5.hexdigest(value + deprecated_password_salt)
    elsif crypt2index.key?(deprecated_password_hash_type)
      value.crypt("$#{crypt2index[deprecated_password_hash_type]}$#{deprecated_password_salt}$").split('$')[3]
    end
  end

  def lookup_strategy
    return UserLdapStrategy.new if Configuration.ldapgroup_enabled?

    UserBasicStrategy.new
  end
end

# == Schema Information
#
# Table name: users
#
#  id                            :integer          not null, primary key
#  adminnote                     :text(65535)
#  biography                     :string(255)      default("")
#  censored                      :boolean          default(FALSE), not null, indexed
#  color_theme                   :integer          default("system"), not null
#  deprecated_password           :string(255)      indexed
#  deprecated_password_hash_type :string(255)
#  deprecated_password_salt      :string(255)
#  email                         :string(200)      default(""), not null
#  ignore_auth_services          :boolean          default(FALSE)
#  in_beta                       :boolean          default(FALSE), indexed
#  in_rollout                    :boolean          default(TRUE), indexed
#  last_logged_in_at             :datetime
#  login                         :text(65535)      indexed
#  login_failure_count           :integer          default(0), not null
#  password_digest               :string(255)
#  realname                      :string(200)      default(""), not null
#  rss_secret                    :string(200)      indexed
#  state                         :string           default("unconfirmed"), indexed
#  created_at                    :datetime
#  updated_at                    :datetime
#  owner_id                      :integer
#
# Indexes
#
#  index_users_on_censored    (censored)
#  index_users_on_in_beta     (in_beta)
#  index_users_on_in_rollout  (in_rollout)
#  index_users_on_rss_secret  (rss_secret) UNIQUE
#  index_users_on_state       (state)
#  users_login_index          (login) UNIQUE
#  users_password_index       (deprecated_password)
#