auth.rb 9.55 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
module Gitlab
2
  module Auth
3
    MissingPersonalAccessTokenError = Class.new(StandardError)
4

5
    REGISTRY_SCOPES = [:read_registry].freeze
6

7
    # Scopes used for GitLab API access
8
    API_SCOPES = [:api, :read_user, :sudo, :read_repository].freeze
9

10
    # Scopes used for OpenID Connect
11
12
    OPENID_SCOPES = [:openid].freeze

13
    # Default scopes for OAuth applications that don't define their own
Douwe Maan's avatar
Douwe Maan committed
14
    DEFAULT_SCOPES = [:api].freeze
15

16
    class << self
17
      def find_for_git_client(login, password, project:, ip:)
18
19
        raise "Must provide an IP for rate limiting" if ip.nil?

20
21
22
        # `user_with_password_for_git` should be the last check
        # because it's the most expensive, especially when LDAP
        # is enabled.
23
        result =
24
          service_request_check(login, password, project) ||
25
          build_access_token_check(login, password) ||
26
          lfs_token_check(login, password, project) ||
27
          oauth_access_token_check(login, password) ||
Simon Vocella's avatar
Simon Vocella committed
28
          personal_access_token_check(password) ||
29
          deploy_token_check(login, password) ||
30
          user_with_password_for_git(login, password) ||
31
          Gitlab::Auth::Result.new
32

33
        rate_limit!(ip, success: result.success?, login: login)
34
        Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
35

36
        return result if result.success? || authenticate_using_internal_or_ldap_password?
37
38
39

        # If sign-in is disabled and LDAP is not configured, recommend a
        # personal access token on failed auth attempts
40
        raise Gitlab::Auth::MissingPersonalAccessTokenError
41
42
      end

43
      def find_with_user_password(login, password)
44
45
        # Avoid resource intensive checks if login credentials are not provided
        return unless login.present? && password.present?
46

47
48
49
50
        # Nothing to do here if internal auth is disabled and LDAP is
        # not configured
        return unless authenticate_using_internal_or_ldap_password?

51
52
        Gitlab::Auth::UniqueIpsLimiter.limit_user! do
          user = User.by_login(login)
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
          return if user && !user.active?

          authenticators = []

          if user
            authenticators << Gitlab::Auth::OAuth::Provider.authentication(user, 'database')

            # Add authenticators for all identities if user is not nil
            user&.identities&.each do |identity|
              authenticators << Gitlab::Auth::OAuth::Provider.authentication(user, identity.provider)
            end
          else
            # If no user is provided, try LDAP.
            #   LDAP users are only authenticated via LDAP
            authenticators << Gitlab::Auth::LDAP::Authentication
69
          end
70
71
72

          authenticators.compact!

73
74
75
76
77
          # return found user that was authenticated first for given login credentials
          authenticators.find do |auth|
            authenticated_user = auth.login(login, password)
            break authenticated_user if authenticated_user
          end
78
79
80
        end
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
      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

102
103
      private

104
      def authenticate_using_internal_or_ldap_password?
105
        Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::Auth::LDAP::Config.enabled?
106
107
      end

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

111
        return unless project && matched_login.present?
112
113
114

        underscored_service = matched_login['service'].underscore

115
        if Service.available_services_names.include?(underscored_service)
116
117
          # We treat underscored_service as a trusted input because it is included
          # in the Service.available_services_names whitelist.
118
          service = project.public_send("#{underscored_service}_service") # rubocop:disable GitlabSecurity/PublicSend
119

120
          if service && service.activated? && service.valid_token?(password)
121
            Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
122
123
124
125
126
127
          end
        end
      end

      def user_with_password_for_git(login, password)
        user = find_with_user_password(login, password)
128
129
        return unless user

130
        raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
131

Z.J. van de Weg's avatar
Z.J. van de Weg committed
132
        Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
133
134
      end

135
136
137
      def oauth_access_token_check(login, password)
        if login == "oauth2" && password.present?
          token = Doorkeeper::AccessToken.by_token(password)
138

139
          if valid_oauth_token?(token)
140
            user = User.find_by(id: token.resource_owner_id)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
141
            Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
142
          end
143
144
        end
      end
145

Simon Vocella's avatar
Simon Vocella committed
146
147
      def personal_access_token_check(password)
        return unless password.present?
148

149
        token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
Simon Vocella's avatar
Simon Vocella committed
150

151
        if token && valid_scoped_token?(token, available_scopes)
152
          Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
153
154
        end
      end
Patricio Cano's avatar
Patricio Cano committed
155

156
      def valid_oauth_token?(token)
157
        token && token.accessible? && valid_scoped_token?(token, [:api])
158
159
      end

Z.J. van de Weg's avatar
Z.J. van de Weg committed
160
      def valid_scoped_token?(token, scopes)
161
162
163
        AccessTokenValidationService.new(token).include_any_scope?(scopes)
      end

164
165
166
      def abilities_for_scopes(scopes)
        abilities_by_scope = {
          api: full_authentication_abilities,
167
          read_registry: build_authentication_abilities - [:build_create_container_image],
168
          read_repository: read_authentication_abilities - [:read_container_image]
169
170
171
172
173
        }

        scopes.flat_map do |scope|
          abilities_by_scope.fetch(scope.to_sym, [])
        end.uniq
174
175
      end

176
177
178
      # Project is always sent when using read_scope,
      # but is not sent when using read_registry scope
      # (since jwt is not context aware of the project)
179
      def deploy_token_check(login, password)
180
        return unless password.present?
181

182
        token =
183
184
185
186
187
188
189
190
          DeployToken.active.find_by(token: password)

        return unless token
        return unless login != "gitlab+deploy-token-#{token.id}"
        
        scopes = abilities_for_scopes(token.scopes)
        if valid_scoped_token?(token, scopes)
          Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes)
191
192
193
        end
      end

194
      def lfs_token_check(login, password, project)
195
196
197
198
199
200
201
202
203
        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

204
        return unless actor
205

206
        token_handler = Gitlab::LfsToken.new(actor)
207

208
209
        authentication_abilities =
          if token_handler.user?
Z.J. van de Weg's avatar
Z.J. van de Weg committed
210
            full_authentication_abilities
211
212
          elsif token_handler.deploy_key_pushable?(project)
            read_write_authentication_abilities
213
          else
Z.J. van de Weg's avatar
Z.J. van de Weg committed
214
            read_authentication_abilities
215
216
          end

217
218
219
        if Devise.secure_compare(token_handler.token, password)
          Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities)
        end
220
221
      end

222
223
224
225
      def build_access_token_check(login, password)
        return unless login == 'gitlab-ci-token'
        return unless password

226
        build = ::Ci::Build.running.find_by_token(password)
227
        return unless build
Kamil Trzcinski's avatar
Kamil Trzcinski committed
228
        return unless build.project.builds_enabled?
229
230
231

        if build.user
          # If user is assigned to build, use restricted credentials of user
232
          Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
233
234
        else
          # Otherwise use generic CI credentials (backward compatibility)
235
          Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
236
237
        end
      end
238

239
240
      public

241
      def build_authentication_abilities
242
243
        [
          :read_project,
244
          :build_download_code,
245
          :build_read_container_image,
246
          :build_create_container_image
247
248
249
        ]
      end

Z.J. van de Weg's avatar
Z.J. van de Weg committed
250
      def read_authentication_abilities
251
252
        [
          :read_project,
253
          :download_code,
254
255
256
257
          :read_container_image
        ]
      end

258
      def read_write_authentication_abilities
Z.J. van de Weg's avatar
Z.J. van de Weg committed
259
        read_authentication_abilities + [
260
          :push_code,
261
262
263
264
265
266
          :create_container_image
        ]
      end

      def full_authentication_abilities
        read_write_authentication_abilities + [
267
          :admin_container_image
268
269
        ]
      end
270

Douwe Maan's avatar
Douwe Maan committed
271
272
273
274
      def available_scopes(current_user = nil)
        scopes = API_SCOPES + registry_scopes
        scopes.delete(:sudo) if current_user && !current_user.admin?
        scopes
275
276
277
278
279
280
281
282
283
284
285
286
      end

      # Other available scopes
      def optional_scopes
        available_scopes + OPENID_SCOPES - DEFAULT_SCOPES
      end

      def registry_scopes
        return [] unless Gitlab.config.registry.enabled

        REGISTRY_SCOPES
      end
287
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
288
289
  end
end