BigW Consortium Gitlab

grack_auth.rb 6.22 KB
Newer Older
1 2
require_relative 'shell_env'

3
module Grack
4 5 6 7 8 9 10 11
  class AuthSpawner
    def self.call(env)
      # Avoid issues with instance variables in Grack::Auth persisting across
      # requests by creating a new instance for each request.
      Auth.new({}).call(env)
    end
  end

12
  class Auth < Rack::Auth::Basic
13

14
    attr_accessor :user, :project, :env
15

16 17 18 19
    def call(env)
      @env = env
      @request = Rack::Request.new(env)
      @auth = Request.new(env)
20

Kirilll Zaitsev committed
21
      @ci = false
22

23
      # Need this patch due to the rails mount
24 25 26 27 28 29 30
      # 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
31

32
      @env['SCRIPT_NAME'] = ""
33

34 35
      auth!

36 37 38
      lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
      return lfs_response unless lfs_response.nil?

39
      if project && authorized_request?
40
        # Tell gitlab-workhorse the request is OK, and what the GL_ID is
41
        render_grack_auth_ok
Kirilll Zaitsev committed
42
      elsif @user.nil? && !@ci
43
        unauthorized
44 45 46
      else
        render_not_found
      end
47
    end
48

49 50 51
    private

    def auth!
52
      return unless @auth.provided?
53

54
      return bad_request unless @auth.basic?
55

56 57
      # Authentication with username and password
      login, password = @auth.credentials
58

59 60
      # Allow authentication for GitLab CI service
      # if valid token passed
Kirilll Zaitsev committed
61 62
      if ci_request?(login, password)
        @ci = true
63
        return
64
      end
65

66 67 68 69 70
      @user = authenticate_user(login, password)

      if @user
        Gitlab::ShellEnv.set_env(@user)
        @env['REMOTE_USER'] = @auth.username
71 72 73
      end
    end

Kirilll Zaitsev committed
74 75
    def ci_request?(login, password)
      matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
76

Kirilll Zaitsev committed
77
      if project && matched_login.present? && git_cmd == 'git-upload-pack'
78
        underscored_service = matched_login['s'].underscore
Kirilll Zaitsev committed
79

80
        if underscored_service == 'gitlab_ci'
Kamil Trzcinski committed
81
          return project && project.valid_build_token?(password)
82
        elsif Service.available_services_names.include?(underscored_service)
Kirilll Zaitsev committed
83 84 85 86
          service_method = "#{underscored_service}_service"
          service = project.send(service_method)

          return service && service.activated? && service.valid_token?(password)
87 88 89 90
        end
      end

      false
91 92
    end

93 94 95 96 97 98 99
    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

100
    def authenticate_user(login, password)
101
      user = Gitlab::Auth.new.find(login, password)
102

103 104 105
      unless user
        user = oauth_access_token_check(login, password)
      end
106

107 108 109 110 111 112 113 114 115 116
      # 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.
117
      config = Gitlab.config.rack_attack.git_basic_auth
118 119 120 121 122

      if config.enabled
        if user
          # A successful login will reset the auth failure count from this IP
          Rack::Attack::Allow2Ban.reset(@request.ip, config)
123
        else
124 125 126 127 128 129 130 131 132 133 134 135 136 137
          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
138
        end
139 140
      end

141
      user
142 143
    end

144
    def authorized_request?
Kirilll Zaitsev committed
145
      return true if @ci
146

147
      case git_cmd
Dmitriy Zaporozhets committed
148
      when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
149 150 151
        if !Gitlab.config.gitlab_shell.upload_pack
          false
        elsif user
152
          Gitlab::GitAccess.new(user, project).download_access_check.allowed?
153 154 155 156 157 158
        elsif project.public?
          # Allow clone/fetch for public projects
          true
        else
          false
        end
Dmitriy Zaporozhets committed
159
      when *Gitlab::GitAccess::PUSH_COMMANDS
160 161 162
        if !Gitlab.config.gitlab_shell.receive_pack
          false
        elsif user
163
          # Skip user authorization on upload request.
164
          # It will be done by the pre-receive hook in the repository.
165 166 167 168
          true
        else
          false
        end
Dmitriy Zaporozhets committed
169 170
      else
        false
171 172
      end
    end
173

174
    def git_cmd
175 176 177 178 179 180 181 182 183
      if @request.get?
        @request.params['service']
      elsif @request.post?
        File.basename(@request.path)
      else
        nil
      end
    end

184
    def project
185 186 187
      return @project if defined?(@project)

      @project = project_by_path(@request.path_info)
188
    end
Dmitriy Zaporozhets committed
189 190 191 192 193 194

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

195
        path_with_namespace[0] = '' if path_with_namespace.start_with?('/')
Dmitriy Zaporozhets committed
196 197 198 199
        Project.find_with_namespace(path_with_namespace)
      end
    end

200
    def render_grack_auth_ok
201 202 203 204 205 206 207
      repo_path =
        if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/
          ProjectWiki.new(project).repository.path_to_repo
        else
          project.repository.path_to_repo
        end

208 209 210 211 212
      [
        200,
        { "Content-Type" => "application/json" },
        [JSON.dump({
          'GL_ID' => Gitlab::ShellEnv.gl_id(@user),
213
          'RepoPath' => repo_path,
214 215
        })]
      ]
216 217
    end

Dmitriy Zaporozhets committed
218
    def render_not_found
219
      [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
Dmitriy Zaporozhets committed
220
    end
221 222
  end
end