change_access.rb 7.81 KB
Newer Older
1
module Gitlab
2 3
  module Checks
    class ChangeAccess
4
      include PathLocksHelper
Valery Sizov's avatar
Valery Sizov committed
5

6 7
      # protocol is currently used only in EE
      attr_reader :user_access, :project, :skip_authorization, :protocol
8

9
      def initialize(
10
        change, user_access:, project:, skip_authorization: false,
11 12
        protocol:
      )
13 14
        @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
        @branch_name = Gitlab::Git.branch_name(@ref)
15
        @tag_name = Gitlab::Git.tag_name(@ref)
16 17
        @user_access = user_access
        @project = project
18
        @skip_authorization = skip_authorization
19
        @protocol = protocol
20 21 22
      end

      def exec
23
        error = push_checks || tag_checks || protected_branch_checks || push_rule_check
24 25 26 27 28 29 30 31 32 33 34

        if error
          GitAccessStatus.new(false, error)
        else
          GitAccessStatus.new(true)
        end
      end

      protected

      def protected_branch_checks
35
        return if skip_authorization
36
        return unless @branch_name
37
        return unless ProtectedBranch.protected?(project, @branch_name)
38

39
        if forced_push?
40
          return "You are not allowed to force push code to a protected branch on this project."
41
        elsif deletion?
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
          return "You are not allowed to delete protected branches from this project."
        end

        if matching_merge_request?
          if user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name)
            return
          else
            "You are not allowed to merge code into protected branches on this project."
          end
        else
          if user_access.can_push_to_branch?(@branch_name)
            return
          else
            "You are not allowed to push code to protected branches on this project."
          end
        end
      end

      def tag_checks
61 62
        return if skip_authorization

63
        return unless @tag_name
64

65
        if tag_exists? && user_access.cannot_do_action?(:admin_project)
66
          return "You are not allowed to change existing tags on this project."
67
        end
68 69 70 71 72 73

        protected_tag_checks
      end

      def protected_tag_checks
        return unless tag_protected?
74 75
        return "Protected tags cannot be updated." if update?
        return "Protected tags cannot be deleted." if deletion?
76

77
        unless user_access.can_create_tag?(@tag_name)
78
          return "You are not allowed to create this tag as it is protected."
79 80 81 82
        end
      end

      def tag_protected?
83
        ProtectedTag.protected?(project, @tag_name)
84 85 86
      end

      def push_checks
87 88
        return if skip_authorization

89 90 91 92 93 94 95
        if user_access.cannot_do_action?(:push_code)
          "You are not allowed to push code to this project."
        end
      end

      private

96 97
      def tag_exists?
        project.repository.tag_exists?(@tag_name)
98 99 100
      end

      def forced_push?
101
        Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
102 103
      end

104 105 106 107 108
      def update?
        !Gitlab::Git.blank_ref?(@oldrev) && !deletion?
      end

      def deletion?
109 110 111
        Gitlab::Git.blank_ref?(@newrev)
      end

112 113 114 115 116
      def matching_merge_request?
        Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
      end

      def push_rule_check
117
        return unless @newrev && @oldrev
118 119 120 121

        push_rule = project.push_rule

        # Prevent tag removal
122
        if @tag_name
123 124
          if tag_deletion_denied_by_push_rule?(push_rule)
            return 'You cannot delete a tag'
125 126
          end
        else
127 128
          commit_validation = push_rule.try(:commit_validation?)

129
          # if newrev is blank, the branch was deleted
130
          return if deletion? || !(commit_validation || validate_path_locks?)
131

132
          commits.each do |commit|
133 134 135
            if commit_validation
              error = check_commit(commit, push_rule)
              return error if error
136 137
            end

138 139
            if error = check_commit_diff(commit, push_rule)
              return error
140 141 142 143 144 145 146
            end
          end
        end

        nil
      end

147 148 149
      def tag_deletion_denied_by_push_rule?(push_rule)
        push_rule.try(:deny_delete_tag) &&
          protocol != 'web' &&
150 151
          deletion? &&
          tag_exists?
152 153
      end

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
      # If commit does not pass push rule validation the whole push should be rejected.
      # This method should return nil if no error found or status object if there are some errors.
      # In case of errors - all other checks will be canceled and push will be rejected.
      def check_commit(commit, push_rule)
        unless push_rule.commit_message_allowed?(commit.safe_message)
          return "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'"
        end

        unless push_rule.author_email_allowed?(commit.committer_email)
          return "Committer's email '#{commit.committer_email}' does not follow the pattern '#{push_rule.author_email_regex}'"
        end

        unless push_rule.author_email_allowed?(commit.author_email)
          return "Author's email '#{commit.author_email}' does not follow the pattern '#{push_rule.author_email_regex}'"
        end

        # Check whether author is a GitLab member
        if push_rule.member_check
          unless User.existing_member?(commit.author_email.downcase)
            return "Author '#{commit.author_email}' is not a member of team"
          end

          if commit.author_email.casecmp(commit.committer_email) == -1
            unless User.existing_member?(commit.committer_email.downcase)
              return "Committer '#{commit.committer_email}' is not a member of team"
            end
          end
        end

        nil
      end

      def check_commit_diff(commit, push_rule)
187 188 189 190
        validations = validations_for_commit(commit, push_rule)

        return if validations.empty?

191
        commit.raw_deltas.each do |diff|
192 193 194
          validations.each do |validation|
            if error = validation.call(diff)
              return error
195 196 197 198
            end
          end
        end

199 200 201 202 203 204 205 206
        nil
      end

      def validations_for_commit(commit, push_rule)
        validations = base_validations

        return validations unless push_rule

207
        validations << file_name_validation(push_rule)
208

209
        if push_rule.max_file_size > 0
210 211
          validations << file_size_validation(commit, push_rule.max_file_size)
        end
212

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        validations
      end

      def base_validations
        validate_path_locks? ? [path_locks_validation] : []
      end

      def validate_path_locks?
        @validate_path_locks ||= license_allows_file_locks? &&
          project.path_locks.any? && @newrev && @oldrev &&
          project.default_branch == @branch_name # locks protect default branch only
      end

      def path_locks_validation
        lambda do |diff|
          path = diff.new_path || diff.old_path

          lock_info = project.find_path_lock(path)

          if lock_info && lock_info.user != user_access.user
            return "The path '#{lock_info.path}' is locked by #{lock_info.user.name}"
234 235
          end
        end
236
      end
237

238
      def file_name_validation(push_rule)
239
        lambda do |diff|
240 241 242 243
          if (diff.renamed_file || diff.new_file) && blacklisted_regex = push_rule.filename_blacklisted?(diff.new_path)
            return nil unless blacklisted_regex.present?

            "File name #{diff.new_path} was blacklisted by the pattern #{blacklisted_regex}."
244 245 246 247 248 249 250 251 252 253 254 255 256
          end
        end
      end

      def file_size_validation(commit, max_file_size)
        lambda do |diff|
          return if diff.deleted_file

          blob = project.repository.blob_at(commit.id, diff.new_path)
          if blob && blob.size && blob.size > max_file_size.megabytes
            return "File #{diff.new_path.inspect} is larger than the allowed size of #{max_file_size} MB"
          end
        end
257 258
      end

259 260
      def commits
        project.repository.new_commits(@newrev)
261 262 263 264
      end
    end
  end
end