grack_auth.rb 4.95 KB
Newer Older
1
require_relative 'rack_attack_helpers'
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
2 3
require_relative 'shell_env'

4 5
module Grack
  class Auth < Rack::Auth::Basic
6

7
    attr_accessor :user, :project, :env
8

9 10 11 12
    def call(env)
      @env = env
      @request = Rack::Request.new(env)
      @auth = Request.new(env)
13

14
      @gitlab_ci = false
15

16
      # Need this patch due to the rails mount
17 18 19 20 21 22 23
      # Need this if under RELATIVE_URL_ROOT
      unless Gitlab.config.gitlab.relative_url_root.empty?
        # If website is mounted using relative_url_root need to remove it first
        @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'')
      else
        @env['PATH_INFO'] = @request.path
      end
24

25
      @env['SCRIPT_NAME'] = ""
26

27 28 29 30 31 32
      auth!

      if project && authorized_request?
        @app.call(env)
      elsif @user.nil? && !@gitlab_ci
        unauthorized
33 34 35
      else
        render_not_found
      end
36
    end
37

38 39 40
    private

    def auth!
41
      return unless @auth.provided?
42

43
      return bad_request unless @auth.basic?
44

45 46
      # Authentication with username and password
      login, password = @auth.credentials
47

48 49 50 51 52
      # Allow authentication for GitLab CI service
      # if valid token passed
      if gitlab_ci_request?(login, password)
        @gitlab_ci = true
        return
53
      end
54

55 56 57 58 59
      @user = authenticate_user(login, password)

      if @user
        Gitlab::ShellEnv.set_env(@user)
        @env['REMOTE_USER'] = @auth.username
60 61 62
      end
    end

63
    def gitlab_ci_request?(login, password)
64
      if login == "gitlab-ci-token" && project && project.gitlab_ci?
65 66 67
        token = project.gitlab_ci_service.token

        if token.present? && token == password && git_cmd == 'git-upload-pack'
68
          return true
69 70 71 72
        end
      end

      false
73 74
    end

75 76 77 78 79 80 81
    def oauth_access_token_check(login, password)
      if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present?
        token = Doorkeeper::AccessToken.by_token(password)
        token && token.accessible? && User.find_by(id: token.resource_owner_id)
      end
    end

82
    def authenticate_user(login, password)
83
      user = Gitlab::Auth.new.find(login, password)
84

85 86 87
      unless user
        user = oauth_access_token_check(login, password)
      end
88

89 90 91 92 93 94 95 96 97 98
      # If the user authenticated successfully, we reset the auth failure count
      # from Rack::Attack for that IP. A client may attempt to authenticate
      # with a username and blank password first, and only after it receives
      # a 401 error does it present a password. Resetting the count prevents
      # false positives from occurring.
      #
      # Otherwise, we let Rack::Attack know there was a failed authentication
      # attempt from this IP. This information is stored in the Rails cache
      # (Redis) and will be used by the Rack::Attack middleware to decide
      # whether to block requests from this IP.
99
      config = Gitlab.config.rack_attack.git_basic_auth
100 101 102 103 104

      if config.enabled
        if user
          # A successful login will reset the auth failure count from this IP
          Rack::Attack::Allow2Ban.reset(@request.ip, config)
105
        else
106 107 108 109 110 111 112 113 114 115 116 117 118 119
          banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
            # Unless the IP is whitelisted, return true so that Allow2Ban
            # increments the counter (stored in Rails.cache) for the IP
            if config.ip_whitelist.include?(@request.ip)
              false
            else
              true
            end
          end

          if banned
            Rails.logger.info "IP #{@request.ip} failed to login " \
              "as #{login} but has been temporarily banned from Git auth"
          end
120
        end
121 122
      end

123
      user
124 125
    end

126
    def authorized_request?
127 128
      return true if @gitlab_ci

129
      case git_cmd
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
130
      when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
131
        if user
132
          Gitlab::GitAccess.new.download_access_check(user, project).allowed?
133 134 135 136 137 138
        elsif project.public?
          # Allow clone/fetch for public projects
          true
        else
          false
        end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
139
      when *Gitlab::GitAccess::PUSH_COMMANDS
140 141
        if user
          # Skip user authorization on upload request.
142
          # It will be done by the pre-receive hook in the repository.
143 144 145 146
          true
        else
          false
        end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
147 148
      else
        false
149 150
      end
    end
151

152
    def git_cmd
153 154 155 156 157 158 159 160 161
      if @request.get?
        @request.params['service']
      elsif @request.post?
        File.basename(@request.path)
      else
        nil
      end
    end

162
    def project
163 164 165
      return @project if defined?(@project)

      @project = project_by_path(@request.path_info)
166
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
167 168 169 170 171 172

    def project_by_path(path)
      if m = /^([\w\.\/-]+)\.git/.match(path).to_a
        path_with_namespace = m.last
        path_with_namespace.gsub!(/\.wiki$/, '')

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
173
        path_with_namespace[0] = '' if path_with_namespace.start_with?('/')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
174 175 176 177 178
        Project.find_with_namespace(path_with_namespace)
      end
    end

    def render_not_found
179
      [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
180
    end
181 182
  end
end