auth.rb 5.59 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
module Gitlab
2
  module Auth
3 4
    class MissingPersonalTokenError < StandardError; end

5
    class << self
6
      def find_for_git_client(login, password, project:, ip:)
7 8
        raise "Must provide an IP for rate limiting" if ip.nil?

9
        result =
10
          service_request_check(login, password, project) ||
11 12 13
          build_access_token_check(login, password) ||
          user_with_password_for_git(login, password) ||
          oauth_access_token_check(login, password) ||
14
          lfs_token_check(login, password) ||
15
          personal_access_token_check(login, password) ||
16
          Gitlab::Auth::Result.new
17

18
        rate_limit!(ip, success: result.success?, login: login)
19

20
        result
21 22
      end

23
      def find_with_user_password(login, password)
24 25 26 27 28 29 30 31 32 33 34 35 36 37
        user = User.by_login(login)

        # If no user is found, or it's an LDAP server, try LDAP.
        #   LDAP users are only authenticated via LDAP
        if user.nil? || user.ldap_user?
          # Second chance - try LDAP authentication
          return nil unless Gitlab::LDAP::Config.enabled?

          Gitlab::LDAP::Authentication.login(login, password)
        else
          user if user.valid_password?(password)
        end
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
      def rate_limit!(ip, success:, login:)
        rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip)
        return unless rate_limiter.enabled?

        if success
          # Repeated login 'failures' are normal behavior for some Git clients so
          # it is important to reset the ban counter once the client has proven
          # they are not a 'bad guy'.
          rate_limiter.reset!
        else
          # Register a login failure so that Rack::Attack can block the next
          # request from this IP if needed.
          rate_limiter.register_fail!

          if rate_limiter.banned?
            Rails.logger.info "IP #{ip} failed to login " \
              "as #{login} but has been temporarily banned from Git auth"
          end
        end
      end

59 60
      private

61
      def service_request_check(login, password, project)
62 63
        matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)

64
        return unless project && matched_login.present?
65 66 67

        underscored_service = matched_login['service'].underscore

68
        if Service.available_services_names.include?(underscored_service)
69 70
          # We treat underscored_service as a trusted input because it is included
          # in the Service.available_services_names whitelist.
Jacob Vosmaer's avatar
Jacob Vosmaer committed
71
          service = project.public_send("#{underscored_service}_service")
72

73
          if service && service.activated? && service.valid_token?(password)
74
            Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
75 76 77 78 79 80
          end
        end
      end

      def user_with_password_for_git(login, password)
        user = find_with_user_password(login, password)
81 82
        return unless user

83
        raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
84

85
        Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
86 87
      end

88 89 90
      def oauth_access_token_check(login, password)
        if login == "oauth2" && password.present?
          token = Doorkeeper::AccessToken.by_token(password)
91 92
          if token && token.accessible?
            user = User.find_by(id: token.resource_owner_id)
93
            Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
94
          end
95 96
        end
      end
97 98 99 100

      def personal_access_token_check(login, password)
        if login && password
          user = User.find_by_personal_access_token(password)
101
          validation = User.by_login(login)
102
          Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
103 104
        end
      end
Patricio Cano's avatar
Patricio Cano committed
105

106 107 108 109 110 111 112 113 114 115
      def lfs_token_check(login, password)
        deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)

        actor =
          if deploy_key_matches
            DeployKey.find(deploy_key_matches[1])
          else
            User.by_login(login)
          end

116
        return unless actor
117

118
        token_handler = Gitlab::LfsToken.new(actor)
119

120 121 122 123 124 125 126
        authentication_abilities =
          if token_handler.user?
            full_authentication_abilities
          else
            read_authentication_abilities
          end

127
        Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
128 129
      end

130 131 132 133
      def build_access_token_check(login, password)
        return unless login == 'gitlab-ci-token'
        return unless password

134
        build = ::Ci::Build.running.find_by_token(password)
135
        return unless build
Kamil Trzcinski's avatar
Kamil Trzcinski committed
136
        return unless build.project.builds_enabled?
137 138 139

        if build.user
          # If user is assigned to build, use restricted credentials of user
140
          Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
141 142
        else
          # Otherwise use generic CI credentials (backward compatibility)
143
          Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
144 145
        end
      end
146

147 148
      public

149
      def build_authentication_abilities
150 151
        [
          :read_project,
152 153 154
          :build_download_code,
          :build_read_container_image,
          :build_create_container_image
155 156 157
        ]
      end

158
      def read_authentication_abilities
159 160
        [
          :read_project,
161
          :download_code,
162 163 164 165
          :read_container_image
        ]
      end

166 167
      def full_authentication_abilities
        read_authentication_abilities + [
168
          :push_code,
169
          :create_container_image
170 171
        ]
      end
172
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
173 174
  end
end