summaryrefslogtreecommitdiff
path: root/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
blob: 0c747ad9c84e12ec0db7b6eb54ec56c2d05a44a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# frozen_string_literal: true

module Preloaders
  # This class preloads the max access level (role) for the user within the given groups and
  # stores the values in requests store.
  class UserMaxAccessLevelInGroupsPreloader
    def initialize(groups, user)
      @groups = groups
      @user = user
    end

    def execute
      if ::Feature.enabled?(:use_traversal_ids)
        preload_with_traversal_ids
      else
        preload_direct_memberships
      end
    end

    private

    def preload_direct_memberships
      group_memberships = GroupMember.active_without_invites_and_requests
                                     .where(user: @user, source_id: @groups)
                                     .group(:source_id)
                                     .maximum(:access_level)

      @groups.each do |group|
        access_level = group_memberships[group.id]
        group.merge_value_to_request_store(User, @user.id, access_level) if access_level.present?
      end
    end

    def preload_with_traversal_ids
      # Diagrammatic representation of this step:
      # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111157#note_1271550140
      max_access_levels = GroupMember.from_union(all_memberships)
                            .joins("INNER JOIN (#{traversal_join_sql}) as hierarchy ON members.source_id = hierarchy.traversal_id")
                            .group('hierarchy.id')
                            .maximum(:access_level)

      @groups.each do |group|
        max_access_level = max_access_levels[group.id] || Gitlab::Access::NO_ACCESS
        group.merge_value_to_request_store(User, @user.id, max_access_level)
      end
    end

    def all_memberships
      if Feature.enabled?(:include_memberships_from_group_shares_in_preloader)
        [
          direct_memberships.select(*GroupMember.cached_column_list),
          memberships_from_group_shares
        ]
      else
        [direct_memberships]
      end
    end

    def direct_memberships
      GroupMember.active_without_invites_and_requests.where(user: @user)
    end

    def memberships_from_group_shares
      alter_direct_memberships_to_make_it_act_like_memberships_in_shared_groups
    end

    def alter_direct_memberships_to_make_it_act_like_memberships_in_shared_groups
      group_group_link_table = GroupGroupLink.arel_table
      group_member_table = GroupMember.arel_table

      altered_columns = GroupMember.attribute_names.map do |column_name|
        case column_name
        when 'access_level'
          # Consider the limiting effect of group share's access level
          smallest_value_arel([group_group_link_table[:group_access], group_member_table[:access_level]], 'access_level')
        when 'source_id'
          # Alter the `source_id` of the `Member` record that is currently pointing to the `shared_with_group`
          # such that this record would now behave like a `Member` record of this user pointing to the `shared_group` group.
          Arel::Nodes::As.new(group_group_link_table[:shared_group_id], Arel::Nodes::SqlLiteral.new('source_id'))
        else
          group_member_table[column_name]
        end
      end

      direct_memberships_in_groups_that_have_been_shared_with_other_groups.select(*altered_columns)
    end

    def direct_memberships_in_groups_that_have_been_shared_with_other_groups
      direct_memberships.joins(
        "INNER JOIN group_group_links ON members.source_id = group_group_links.shared_with_group_id"
      )
    end

    def smallest_value_arel(args, column_alias)
      Arel::Nodes::As.new(
        Arel::Nodes::NamedFunction.new('LEAST', args),
        Arel::Nodes::SqlLiteral.new(column_alias))
    end

    def traversal_join_sql
      Namespace.select('id, unnest(traversal_ids) as traversal_id').where(id: @groups.map(&:id)).to_sql
    end
  end
end