Class: Member

Inherits:
ApplicationRecord show all
Extended by:
Gitlab::Utils::Override
Includes:
AfterCommitQueue, Ci::PipelineScheduleOwnershipValidator, 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_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, Gitlab::Access::SECURITY_MANAGER

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 Ci::PipelineScheduleOwnershipValidator

#notify_and_disable_all_pipeline_schedules_for_user

Methods included from Presentable

#present

Methods included from Gitlab::Access

all_keys, all_values, global_protection_levels, #human_access, human_access, #human_access_labeled, human_access_with_none, #human_access_with_none, level_encompasses?, 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, current_transaction, declarative_enum, default_select_columns, delete_all_returning, #deleted_from_database?, id_in, id_not_in, iid_in, nullable_column?, 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 Organizations::Sharding

#sharding_organization

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.



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

def raw_invite_token
  @raw_invite_token
end

Class Method Details

.access_for_user_ids(user_ids) ⇒ Object



455
456
457
# File 'app/models/member.rb', line 455

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



472
473
474
# File 'app/models/member.rb', line 472

def coerce_to_no_access
  select(member_columns_with_no_access)
end

.filter_by_2fa(value) ⇒ Object



416
417
418
419
420
421
422
423
424
425
# File 'app/models/member.rb', line 416

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



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

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



459
460
461
462
# File 'app/models/member.rb', line 459

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



450
451
452
453
# File 'app/models/member.rb', line 450

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

.null_member_role_id_sqlObject



484
485
486
# File 'app/models/member.rb', line 484

def null_member_role_id_sql
  Arel::Nodes::As.new(Arel::Nodes::SqlLiteral.new('NULL'), Arel::Nodes::SqlLiteral.new('member_role_id'))
end

.pluck_user_idsObject



468
469
470
# File 'app/models/member.rb', line 468

def pluck_user_ids
  pluck(:user_id)
end

.search(query) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'app/models/member.rb', line 390

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



412
413
414
# File 'app/models/member.rb', line 412

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

.seat_assignable?(user:, namespace: nil) ⇒ Boolean

Returns:

  • (Boolean)


89
90
91
# File 'app/models/member.rb', line 89

def self.seat_assignable?(user:, namespace: nil)
  seat_assignable(users: user, namespace: namespace).exists?
end

.seat_assignable_highest_access_level(user:, namespace: nil) ⇒ Object



93
94
95
# File 'app/models/member.rb', line 93

def self.seat_assignable_highest_access_level(user:, namespace: nil)
  seat_assignable(users: user, namespace: namespace).maximum(:access_level)
end

.seat_assignable_highest_access_levels(users:, namespace: nil) ⇒ Object



97
98
99
# File 'app/models/member.rb', line 97

def self.seat_assignable_highest_access_levels(users:, namespace: nil)
  seat_assignable(users: users, namespace: namespace).group(:user_id).maximum(:access_level)
end

.shared_members(group) ⇒ Object



476
477
478
479
480
481
482
# File 'app/models/member.rb', line 476

def shared_members(group)
  columns = member_columns_for_shared_members(group)

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

.sort_by_attribute(method) ⇒ Object



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'app/models/member.rb', line 433

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)


464
465
466
# File 'app/models/member.rb', line 464

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

Instance Method Details

#accept_invite!(new_user) ⇒ Object



559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'app/models/member.rb', line 559

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



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

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



528
529
530
# File 'app/models/member.rb', line 528

def access_field
  access_level
end

#create_notification_settingObject



614
615
616
# File 'app/models/member.rb', line 614

def create_notification_setting
  user.notification_settings.find_or_create_for(source)
end

#created_by_nameObject



651
652
653
# File 'app/models/member.rb', line 651

def created_by_name
  created_by&.name
end

#decline_invite!Object



576
577
578
579
580
581
582
583
584
# File 'app/models/member.rb', line 576

def decline_invite!
  return false unless invite?

  destroyed = self.destroy

  after_decline_invite if destroyed

  destroyed
end

#destroy_notification_settingObject



618
619
620
# File 'app/models/member.rb', line 618

def destroy_notification_setting
  notification_setting&.destroy
end

#generate_invite_tokenObject



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

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



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

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



636
637
638
639
640
641
642
643
644
645
# File 'app/models/member.rb', line 636

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)


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

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)


532
533
534
# File 'app/models/member.rb', line 532

def invite?
  self.invite_token.present?
end

#invite_to_unknown_user?Boolean

Returns:

  • (Boolean)


647
648
649
# File 'app/models/member.rb', line 647

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

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

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)


627
628
629
630
631
632
# File 'app/models/member.rb', line 627

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



622
623
624
# File 'app/models/member.rb', line 622

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

#pending?Boolean

Returns:

  • (Boolean)


540
541
542
# File 'app/models/member.rb', line 540

def pending?
  invite? || request?
end

#prevent_role_assignement?(current_user, params) ⇒ Boolean

Returns:

  • (Boolean)


666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'app/models/member.rb', line 666

def prevent_role_assignement?(current_user, params)
  return false if current_user.can_admin_all_resources?

  # access_level will already be set for accepting access request invites
  assigning_access_level = params[:access_level] || access_level
  current_access_level = params[:current_access_level]

  # check if it's a valid downgrade, if the member's current access level encompasses the target level
  return false if Gitlab::Access.level_encompasses?(
    current_access_level: current_access_level,
    level_to_assign: assigning_access_level
  )

  !source.can_assign_role?(current_user, assigning_access_level)
end

#real_source_typeObject



524
525
526
# File 'app/models/member.rb', line 524

def real_source_type
  source_type
end

#request?Boolean

Returns:

  • (Boolean)


536
537
538
# File 'app/models/member.rb', line 536

def request?
  requested_at.present?
end

#resend_inviteObject



596
597
598
599
600
601
602
# File 'app/models/member.rb', line 596

def resend_invite
  return unless invite?

  generate_invite_token! unless @raw_invite_token

  send_invite
end

#send_invitation_reminder(reminder_index) ⇒ Object



604
605
606
607
608
609
610
611
612
# File 'app/models/member.rb', line 604

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



655
656
657
658
659
660
661
662
663
664
# File 'app/models/member.rb', line 655

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