summaryrefslogtreecommitdiff
path: root/lib/gitlab/project_authorizations.rb
blob: 23e380b3cf17ff5e3f1ab577ce12e3d29720f642 (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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# frozen_string_literal: true

# This class relies on Common Table Expressions to efficiently get all data,
# including data for nested groups.
module Gitlab
  class ProjectAuthorizations
    attr_reader :user

    # user - The User object for which to calculate the authorizations.
    def initialize(user)
      @user = user
    end

    def calculate
      cte = recursive_cte
      cte_alias = cte.table.alias(Group.table_name)
      projects = Project.arel_table
      links = ProjectGroupLink.arel_table

      relations = [
        # The project a user has direct access to.
        user.projects.select_for_project_authorization,

        # The personal projects of the user.
        user.personal_projects.select_as_maintainer_for_project_authorization,

        # Projects that belong directly to any of the groups the user has
        # access to.
        Namespace
          .unscoped
          .select([alias_as_column(projects[:id], 'project_id'),
                   cte_alias[:access_level]])
          .from(cte_alias)
          .joins(:projects),

        # Projects shared with any of the namespaces the user has access to.
        Namespace
          .unscoped
          .select([
            links[:project_id],
            least(cte_alias[:access_level], links[:group_access], 'access_level')
          ])
          .from(cte_alias)
          .joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id')
          .joins('INNER JOIN projects ON projects.id = project_group_links.project_id')
          .joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id')
          .where('p_ns.share_with_group_lock IS FALSE')
      ]

      ProjectAuthorization
        .unscoped
        .with
        .recursive(cte.to_arel)
        .select_from_union(relations)
    end

    private

    # Builds a recursive CTE that gets all the groups the current user has
    # access to, including any nested groups and any shared groups.
    def recursive_cte
      cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte)
      members = Member.arel_table
      namespaces = Namespace.arel_table
      group_group_links = GroupGroupLink.arel_table

      # Namespaces the user is a member of.
      cte << user.groups
        .select([namespaces[:id], members[:access_level]])
        .except(:order)

      # Namespaces shared with any of the group
      cte << Group.select([namespaces[:id],
                           least(members[:access_level],
                                 group_group_links[:group_access],
                                 'access_level')])
                  .joins(join_group_group_links)
                  .joins(join_members_on_group_group_links)

      # Sub groups of any groups the user is a member of.
      cte << Group.select([
          namespaces[:id],
          greatest(members[:access_level], cte.table[:access_level], 'access_level')
        ])
        .joins(join_cte(cte))
        .joins(join_members_on_namespaces)
        .except(:order)

      cte
    end

    # Builds a LEFT JOIN to join optional memberships onto the CTE.
    def join_members_on_namespaces
      members = Member.arel_table
      namespaces = Namespace.arel_table

      cond = members[:source_id]
        .eq(namespaces[:id])
        .and(members[:source_type].eq('Namespace'))
        .and(members[:requested_at].eq(nil))
        .and(members[:user_id].eq(user.id))
        .and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))

      Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond))
    end

    def join_group_group_links
      group_group_links = GroupGroupLink.arel_table
      namespaces = Namespace.arel_table

      cond = group_group_links[:shared_group_id].eq(namespaces[:id])
      Arel::Nodes::InnerJoin.new(group_group_links, Arel::Nodes::On.new(cond))
    end

    def join_members_on_group_group_links
      group_group_links = GroupGroupLink.arel_table
      members = Member.arel_table

      cond = group_group_links[:shared_with_group_id].eq(members[:source_id])
                    .and(members[:source_type].eq('Namespace'))
                    .and(members[:requested_at].eq(nil))
                    .and(members[:user_id].eq(user.id))
                    .and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS))
      Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond))
    end

    # Builds an INNER JOIN to join namespaces onto the CTE.
    def join_cte(cte)
      namespaces = Namespace.arel_table
      cond = cte.table[:id].eq(namespaces[:parent_id])

      Arel::Nodes::InnerJoin.new(cte.table, Arel::Nodes::On.new(cond))
    end

    def greatest(left, right, column_alias)
      sql_function('GREATEST', [left, right], column_alias)
    end

    def least(left, right, column_alias)
      sql_function('LEAST', [left, right], column_alias)
    end

    def sql_function(name, args, column_alias)
      alias_as_column(Arel::Nodes::NamedFunction.new(name, args), column_alias)
    end

    def alias_as_column(value, alias_to)
      Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to))
    end
  end
end