summaryrefslogtreecommitdiff
path: root/lib/gitlab/background_migration/remove_restricted_todos.rb
blob: 9ef6d8654ae07d3ad7c460e6a4c60be87e891490 (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
152
153
154
155
156
157
158
# frozen_string_literal: true
# rubocop:disable Style/Documentation
# rubocop:disable Metrics/ClassLength

module Gitlab
  module BackgroundMigration
    class RemoveRestrictedTodos
      PRIVATE_FEATURE = 10
      PRIVATE_PROJECT = 0

      class Project < ActiveRecord::Base
        self.table_name = 'projects'
      end

      class ProjectAuthorization < ActiveRecord::Base
        self.table_name = 'project_authorizations'
      end

      class ProjectFeature < ActiveRecord::Base
        self.table_name = 'project_features'
      end

      class Todo < ActiveRecord::Base
        include EachBatch

        self.table_name = 'todos'
      end

      class Issue < ActiveRecord::Base
        include EachBatch

        self.table_name = 'issues'
      end

      def perform(start_id, stop_id)
        projects = Project.where('EXISTS (SELECT 1 FROM todos WHERE todos.project_id = projects.id)')
          .where(id: start_id..stop_id)

        projects.each do |project|
          remove_confidential_issue_todos(project.id)

          if project.visibility_level == PRIVATE_PROJECT
            remove_non_members_todos(project.id)
          else
            remove_restricted_features_todos(project.id)
          end
        end
      end

      private

      def remove_non_members_todos(project_id)
        batch_remove_todos_cte(project_id)
      end

      def remove_confidential_issue_todos(project_id)
        # min access level to access a confidential issue is reporter
        min_reporters = authorized_users(project_id)
          .select(:user_id)
          .where('access_level >= ?', 20)

        confidential_issues = Issue.select(:id, :author_id).where(confidential: true, project_id: project_id)
        confidential_issues.each_batch(of: 100, order_hint: :confidential) do |batch|
          batch.each do |issue|
            assigned_users = IssueAssignee.select(:user_id).where(issue_id: issue.id)

            todos = Todo.where(target_type: 'Issue', target_id: issue.id)
              .where('user_id NOT IN (?)', min_reporters)
              .where('user_id NOT IN (?)', assigned_users)
            todos = todos.where('user_id != ?', issue.author_id) if issue.author_id

            todos.delete_all
          end
        end
      end

      def remove_restricted_features_todos(project_id)
        ProjectFeature.where(project_id: project_id).each do |project_features|
          target_types = []
          target_types << 'Issue' if private?(project_features.issues_access_level)
          target_types << 'MergeRequest' if private?(project_features.merge_requests_access_level)
          target_types << 'Commit' if private?(project_features.repository_access_level)

          next if target_types.empty?

          batch_remove_todos_cte(project_id, target_types)
        end
      end

      def private?(feature_level)
        feature_level == PRIVATE_FEATURE
      end

      def authorized_users(project_id)
        ProjectAuthorization.select(:user_id).where(project_id: project_id)
      end

      def unauthorized_project_todos(project_id)
        Todo.where(project_id: project_id)
          .where('user_id NOT IN (?)', authorized_users(project_id))
      end

      def batch_remove_todos_cte(project_id, target_types = nil)
        loop do
          count = remove_todos_cte(project_id, target_types)

          break if count == 0
        end
      end

      def remove_todos_cte(project_id, target_types = nil)
        sql = []
        sql << with_all_todos_sql(project_id, target_types)
        sql << as_deleted_sql
        sql << "SELECT count(*) FROM deleted"

        result = Todo.connection.exec_query(sql.join(' '))
        result.rows[0][0].to_i
      end

      def with_all_todos_sql(project_id, target_types = nil)
        if target_types
          table = Arel::Table.new(:todos)
          in_target = table[:target_type].in(target_types)
          target_types_sql = " AND #{in_target.to_sql}"
        end

        <<-SQL
          WITH all_todos AS (
          	SELECT id
          	FROM "todos"
          	WHERE "todos"."project_id" = #{project_id}
            AND (user_id NOT IN (
              SELECT "project_authorizations"."user_id"
              FROM "project_authorizations"
              WHERE "project_authorizations"."project_id" = #{project_id})
              #{target_types_sql}
            )
        	),
        SQL
      end

      def as_deleted_sql
        <<-SQL
          deleted AS (
            DELETE FROM todos
            WHERE id IN (
              SELECT id
              FROM all_todos
              LIMIT 5000
            )
            RETURNING id
          )
        SQL
      end
    end
  end
end