Class: Member

Inherits:
ApplicationRecord show all
Extended by:
Gitlab::Utils::Override
Includes:
AfterCommitQueue, CreatedAtFilterable, EachBatch, Expirable, FromUnion, Gitlab::Access, Gitlab::Experiment::Dsl, Gitlab::Utils::StrongMemoize, Importable, Presentable, RestrictedSignup, Sortable, UpdateHighestRole
Defined in:
app/models/member.rb

Direct Known Subclasses

GroupMember, ProjectMember

Constant Summary collapse

AVATAR_SIZE =
40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT =
10
STATE_ACTIVE =
0
STATE_AWAITING =
1

Constants included from UpdateHighestRole

UpdateHighestRole::HIGHEST_ROLE_JOB_DELAY, UpdateHighestRole::HIGHEST_ROLE_LEASE_TIMEOUT

Constants included from Gitlab::Access

Gitlab::Access::ADMIN, Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS, Gitlab::Access::AccessDeniedError, Gitlab::Access::DEVELOPER, Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS, Gitlab::Access::GUEST, Gitlab::Access::MAINTAINER, Gitlab::Access::MAINTAINER_PROJECT_ACCESS, Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS, Gitlab::Access::MINIMAL_ACCESS, Gitlab::Access::NO_ACCESS, Gitlab::Access::NO_ONE_PROJECT_ACCESS, Gitlab::Access::OWNER, Gitlab::Access::OWNER_PROJECT_ACCESS, Gitlab::Access::OWNER_SUBGROUP_ACCESS, Gitlab::Access::PLANNER, Gitlab::Access::PROTECTION_DEV_CAN_INITIAL_PUSH, Gitlab::Access::PROTECTION_DEV_CAN_MERGE, Gitlab::Access::PROTECTION_DEV_CAN_PUSH, Gitlab::Access::PROTECTION_FULL, Gitlab::Access::PROTECTION_NONE, Gitlab::Access::REPORTER

Constants included from Expirable

Expirable::DAYS_TO_EXPIRE

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from HasCheckConstraints

HasCheckConstraints::NOT_NULL_CHECK_PATTERN

Constants included from ResetOnColumnErrors

ResetOnColumnErrors::MAX_RESET_PERIOD

Instance Attribute Summary collapse

Attributes included from Importable

#importing, #user_contributions

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from Presentable

#present

Methods included from Gitlab::Access

all_values, global_protection_levels, human_access, #human_access, #human_access_labeled, #human_access_with_none, human_access_with_none, option_descriptions, options, options_with_none, options_with_owner, #owner?, project_creation_level_name, project_creation_options, project_creation_string_options, project_creation_string_values, project_creation_values, protection_options, protection_values, role_description, #role_description, subgroup_creation_options, subgroup_creation_string_options, subgroup_creation_string_values, subgroup_creation_values, sym_options, sym_options_with_admin, sym_options_with_owner

Methods included from Expirable

#expired?, #expires?, #expires_soon?

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods inherited from ApplicationRecord

===, cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, nullable_column?, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from ResetOnColumnErrors

#reset_on_union_error, #reset_on_unknown_attribute_error

Methods included from Gitlab::SensitiveSerializableHash

#serializable_hash

Instance Attribute Details

#raw_invite_tokenObject

Returns the value of attribute raw_invite_token.



27
28
29
# File 'app/models/member.rb', line 27

def raw_invite_token
  @raw_invite_token
end

Class Method Details

.access_for_user_ids(user_ids) ⇒ Object



424
425
426
# File 'app/models/member.rb', line 424

def access_for_user_ids(user_ids)
  with_user(user_ids).has_access.pluck(:user_id, :access_level).to_h
end

.coerce_to_no_accessObject



441
442
443
# File 'app/models/member.rb', line 441

def coerce_to_no_access
  select(member_columns_with_no_access)
end

.filter_by_2fa(value) ⇒ Object



385
386
387
388
389
390
391
392
393
394
# File 'app/models/member.rb', line 385

def filter_by_2fa(value)
  case value
  when 'enabled'
    left_join_users.merge(User.with_two_factor)
  when 'disabled'
    left_join_users.merge(User.without_two_factor)
  else
    all
  end
end

.filter_by_user_type(value) ⇒ Object



396
397
398
399
400
# File 'app/models/member.rb', line 396

def filter_by_user_type(value)
  return unless ::User.user_types.key?(value)

  left_join_users.merge(::User.where(user_type: value))
end

.find_by_invite_token(raw_invite_token) ⇒ Object



428
429
430
431
# File 'app/models/member.rb', line 428

def find_by_invite_token(raw_invite_token)
  invite_token = Devise.token_generator.digest(self, :invite_token, raw_invite_token)
  find_by(invite_token: invite_token)
end

.left_join_usersObject



419
420
421
422
# File 'app/models/member.rb', line 419

def left_join_users
  left_outer_joins(:user)
    .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")
end

.pluck_user_idsObject



437
438
439
# File 'app/models/member.rb', line 437

def pluck_user_ids
  pluck(:user_id)
end

.search(query) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'app/models/member.rb', line 359

def search(query)
  scope = joins(:user)
            .merge(User.search(query, use_minimum_char_limit: false))
            .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417456")

  return scope unless Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)

  # If the User.search method returns keyset pagination aware AR scope then we
  # need call apply_cursor_conditions which adds the ORDER BY columns from the scope
  # to the SELECT clause.
  #
  # Why is this needed:
  # When using keyset pagination, the next page is loaded using the ORDER BY
  # values of the last record (cursor). This query selects `members.*` and
  # orders by a custom SQL expression on `users` and `users.name`. The values
  # will not be part of `members.*`.
  #
  # Result: `SELECT members.*, users.column1, users.column2 FROM members ...`
  order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
  order.apply_cursor_conditions(scope).reorder(order)
end

.search_invite_email(query) ⇒ Object



381
382
383
# File 'app/models/member.rb', line 381

def search_invite_email(query)
  invite.where(['invite_email ILIKE ?', "%#{query}%"])
end

.sort_by_attribute(method) ⇒ Object



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'app/models/member.rb', line 402

def sort_by_attribute(method)
  case method.to_s
  when 'access_level_asc' then reorder(access_level: :asc)
  when 'access_level_desc' then reorder(access_level: :desc)
  when 'recent_sign_in' then 
  when 'oldest_sign_in' then 
  when 'recent_created_user' then order_recent_created_user
  when 'oldest_created_user' then order_oldest_created_user
  when 'recent_last_activity' then order_recent_last_activity
  when 'oldest_last_activity' then order_oldest_last_activity
  when 'last_joined' then order_created_desc
  when 'oldest_joined' then order_created_asc
  else
    order_by(method)
  end
end

.valid_email?(email) ⇒ Boolean

Returns:

  • (Boolean)


433
434
435
# File 'app/models/member.rb', line 433

def valid_email?(email)
  Devise.email_regexp.match?(email)
end

.with_group_group_sharing_access(shared_groups, custom_role_for_group_link_enabled) ⇒ Object



445
446
447
448
449
450
451
# File 'app/models/member.rb', line 445

def with_group_group_sharing_access(shared_groups, custom_role_for_group_link_enabled)
  columns = member_columns_with_group_sharing_access(custom_role_for_group_link_enabled)

  joins("LEFT OUTER JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id")
    .select(columns)
    .where(group_group_links: { shared_group_id: shared_groups })
end

Instance Method Details

#accept_invite!(new_user) ⇒ Object



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'app/models/member.rb', line 524

def accept_invite!(new_user)
  return false unless invite?
  return false unless new_user

  self.user = new_user
  return false unless self.user.save

  self.invite_token = nil
  self.invite_accepted_at = Time.current.utc

  saved = self.save

  after_accept_invite if saved

  saved
end

#accept_request(current_user) ⇒ Object



515
516
517
518
519
520
521
522
# File 'app/models/member.rb', line 515

def accept_request(current_user)
  return false unless request?

  updated = self.update(requested_at: nil, created_by: current_user, request_accepted_at: Time.current.utc)
  after_accept_request if updated

  updated
end

#access_fieldObject



493
494
495
# File 'app/models/member.rb', line 493

def access_field
  access_level
end

#create_notification_settingObject



579
580
581
# File 'app/models/member.rb', line 579

def create_notification_setting
  user.notification_settings.find_or_create_for(source)
end

#created_by_nameObject



616
617
618
# File 'app/models/member.rb', line 616

def created_by_name
  created_by&.name
end

#decline_invite!Object



541
542
543
544
545
546
547
548
549
# File 'app/models/member.rb', line 541

def decline_invite!
  return false unless invite?

  destroyed = self.destroy

  after_decline_invite if destroyed

  destroyed
end

#destroy_notification_settingObject



583
584
585
# File 'app/models/member.rb', line 583

def destroy_notification_setting
  notification_setting&.destroy
end

#generate_invite_tokenObject



551
552
553
554
555
# File 'app/models/member.rb', line 551

def generate_invite_token
  raw, enc = Devise.token_generator.generate(self.class, :invite_token)
  @raw_invite_token = raw
  self.invite_token = enc
end

#generate_invite_token!Object



557
558
559
# File 'app/models/member.rb', line 557

def generate_invite_token!
  generate_invite_token && save(validate: false)
end

#highest_group_memberObject

Find the user’s group member with a highest access level



601
602
603
604
605
606
607
608
609
610
# File 'app/models/member.rb', line 601

def highest_group_member
  strong_memoize(:highest_group_member) do
    next unless user_id && source&.ancestors&.any?

    GroupMember
      .where(source: source.ancestors, user_id: user_id)
      .non_request
      .order(:access_level).last
  end
end

#hook_prerequisites_met?Boolean

Returns:

  • (Boolean)


509
510
511
512
513
# File 'app/models/member.rb', line 509

def hook_prerequisites_met?
  # It is essential that an associated user record exists
  # so that we can successfully fire any member related hooks/notifications.
  user.present?
end

#invite?Boolean

Returns:

  • (Boolean)


497
498
499
# File 'app/models/member.rb', line 497

def invite?
  self.invite_token.present?
end

#invite_to_unknown_user?Boolean

Returns:

  • (Boolean)


612
613
614
# File 'app/models/member.rb', line 612

def invite_to_unknown_user?
  invite? && user_id.nil?
end

#notifiable?(type, opts = {}) ⇒ Boolean

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)


592
593
594
595
596
597
# File 'app/models/member.rb', line 592

def notifiable?(type, opts = {})
  # always notify when there isn't a user yet
  return true if user.blank?

  NotificationRecipients::BuildService.notifiable?(user, type, notifiable_options.merge(opts))
end

#notification_settingObject



587
588
589
# File 'app/models/member.rb', line 587

def notification_setting
  @notification_setting ||= user&.notification_settings_for(source)
end

#pending?Boolean

Returns:

  • (Boolean)


505
506
507
# File 'app/models/member.rb', line 505

def pending?
  invite? || request?
end

#prevent_role_assignement?(_current_user, _params) ⇒ Boolean

Returns:

  • (Boolean)


631
632
633
# File 'app/models/member.rb', line 631

def prevent_role_assignement?(_current_user, _params)
  false
end

#real_source_typeObject



489
490
491
# File 'app/models/member.rb', line 489

def real_source_type
  source_type
end

#request?Boolean

Returns:

  • (Boolean)


501
502
503
# File 'app/models/member.rb', line 501

def request?
  requested_at.present?
end

#resend_inviteObject



561
562
563
564
565
566
567
# File 'app/models/member.rb', line 561

def resend_invite
  return unless invite?

  generate_invite_token! unless @raw_invite_token

  send_invite
end

#send_invitation_reminder(reminder_index) ⇒ Object



569
570
571
572
573
574
575
576
577
# File 'app/models/member.rb', line 569

def send_invitation_reminder(reminder_index)
  return unless invite?

  generate_invite_token! unless @raw_invite_token

  run_after_commit_or_now do
    Members::InviteReminderMailer.email(self, @raw_invite_token, reminder_index).deliver_later
  end
end

#update_two_factor_requirementObject



620
621
622
623
624
625
626
627
628
629
# File 'app/models/member.rb', line 620

def update_two_factor_requirement
  return unless source.is_a?(Group)
  return unless user

  Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
    %w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288'
  ) do
    user.update_two_factor_requirement
  end
end