21621: Fix accidentally passing through tabs prop to Tabs component
[arvados.git] / services / api / app / models / api_client_authorization.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class ApiClientAuthorization < ArvadosModel
6   include HasUuid
7   include KindAndEtag
8   include CommonApiTemplate
9   include Rails.application.routes.url_helpers
10   extend CurrentApiClient
11   extend DbCurrentTime
12
13   belongs_to :api_client, optional: true
14   belongs_to :user, optional: true
15   after_initialize :assign_random_api_token
16   serialize :scopes, Array
17
18   before_validation :clamp_token_expiration
19
20   api_accessible :user, extend: :common do |t|
21     t.add :owner_uuid
22     t.add :api_client_id
23     # NB the "api_token" db column is a misnomer in that it's only the
24     # "secret" part of a token: a v1 token is just the secret, but a
25     # v2 token is "v2/uuid/secret".
26     t.add :api_token
27     t.add :created_by_ip_address
28     t.add :expires_at
29     t.add :last_used_at
30     t.add :last_used_by_ip_address
31     t.add :scopes
32   end
33
34   UNLOGGED_CHANGES = ['last_used_at', 'last_used_by_ip_address', 'updated_at']
35
36   def assign_random_api_token
37     begin
38       self.api_token ||= rand(2**256).to_s(36)
39     rescue ActiveModel::MissingAttributeError
40       # Ignore the case where self.api_token doesn't exist, which happens when
41       # the select=[...] is used.
42     end
43   end
44
45   def owner_uuid
46     self.user.andand.uuid
47   end
48   def owner_uuid_was
49     self.user_id_changed? ? User.where(id: self.user_id_was).first.andand.uuid : self.user.andand.uuid
50   end
51   def owner_uuid_changed?
52     self.user_id_changed?
53   end
54
55   def modified_by_client_uuid
56     nil
57   end
58   def modified_by_client_uuid=(x) end
59
60   def modified_by_user_uuid
61     nil
62   end
63   def modified_by_user_uuid=(x) end
64
65   def modified_at
66     nil
67   end
68   def modified_at=(x) end
69
70   def scopes_allow?(req_s)
71     scopes.each do |scope|
72       return true if (scope == 'all') or (scope == req_s) or
73         ((scope.end_with? '/') and (req_s.start_with? scope))
74     end
75     false
76   end
77
78   def scopes_allow_request?(request)
79     method = request.request_method
80     if method == 'GET' and request.path == url_for(controller: 'arvados/v1/api_client_authorizations', action: 'current', only_path: true)
81       true
82     elsif method == 'HEAD'
83       (scopes_allow?(['HEAD', request.path].join(' ')) ||
84        scopes_allow?(['GET', request.path].join(' ')))
85     else
86       scopes_allow?([method, request.path].join(' '))
87     end
88   end
89
90   def logged_attributes
91     super.except 'api_token'
92   end
93
94   def self.default_orders
95     ["#{table_name}.id desc"]
96   end
97
98   def self.remote_host(uuid_prefix:)
99     (Rails.configuration.RemoteClusters[uuid_prefix].andand["Host"]) ||
100       (Rails.configuration.RemoteClusters["*"]["Proxy"] &&
101        uuid_prefix+".arvadosapi.com")
102   end
103
104   def self.make_http_client(uuid_prefix:)
105     clnt = HTTPClient.new
106
107     if uuid_prefix && (Rails.configuration.RemoteClusters[uuid_prefix].andand.Insecure ||
108                        Rails.configuration.RemoteClusters['*'].andand.Insecure)
109       clnt.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
110     else
111       # Use system CA certificates
112       ["/etc/ssl/certs/ca-certificates.crt",
113        "/etc/pki/tls/certs/ca-bundle.crt"]
114         .select { |ca_path| File.readable?(ca_path) }
115         .each { |ca_path| clnt.ssl_config.add_trust_ca(ca_path) }
116     end
117     clnt
118   end
119
120   def self.check_anonymous_user_token(token:, remote:)
121     case token[0..2]
122     when 'v2/'
123       _, token_uuid, secret, optional = token.split('/')
124       unless token_uuid.andand.length == 27 && secret.andand.length.andand > 0 &&
125              token_uuid == Rails.configuration.ClusterID+"-gj3su-anonymouspublic"
126         # invalid v2 token, or v2 token for another user
127         return nil
128       end
129     else
130       # v1 token
131       secret = token
132     end
133
134     # Usually, the secret is salted
135     salted_secret = OpenSSL::HMAC.hexdigest('sha1', Rails.configuration.Users.AnonymousUserToken, remote)
136
137     # The anonymous token could be specified as a full v2 token in the config,
138     # but the config loader strips it down to the secret part.
139     # The anonymous token content and minimum length is verified in lib/config
140     if secret.length >= 0 && (secret == Rails.configuration.Users.AnonymousUserToken || secret == salted_secret)
141       return ApiClientAuthorization.new(user: User.find_by_uuid(anonymous_user_uuid),
142                                         uuid: Rails.configuration.ClusterID+"-gj3su-anonymouspublic",
143                                         api_token: secret,
144                                         api_client: anonymous_user_token_api_client,
145                                         scopes: ['GET /'])
146     else
147       return nil
148     end
149   end
150
151   def self.check_system_root_token token
152     if token == Rails.configuration.SystemRootToken
153       return ApiClientAuthorization.new(user: User.find_by_uuid(system_user_uuid),
154                                         uuid: Rails.configuration.ClusterID+"-gj3su-000000000000000",
155                                         api_token: token,
156                                         api_client: system_root_token_api_client)
157     else
158       return nil
159     end
160   end
161
162   def self.validate(token:, remote: nil)
163     return nil if token.nil? or token.empty?
164     remote ||= Rails.configuration.ClusterID
165
166     auth = self.check_anonymous_user_token(token: token, remote: remote)
167     if !auth.nil?
168       return auth
169     end
170
171     auth = self.check_system_root_token(token)
172     if !auth.nil?
173       return auth
174     end
175
176     token_uuid = ''
177     secret = token
178     stored_secret = nil         # ...if different from secret
179     optional = nil
180
181     case token[0..2]
182     when 'v2/'
183       _, token_uuid, secret, optional = token.split('/')
184       unless token_uuid.andand.length == 27 && secret.andand.length.andand > 0
185         return nil
186       end
187
188       if !optional.nil?
189         # if "optional" is a container uuid, check that it
190         # matches expections.
191         c = Container.where(uuid: optional).first
192         if !c.nil?
193           if !c.auth_uuid.nil? and c.auth_uuid != token_uuid
194             # token doesn't match the container's token
195             return nil
196           end
197           if !c.runtime_token.nil? and "v2/#{token_uuid}/#{secret}" != c.runtime_token
198             # token doesn't match the container's token
199             return nil
200           end
201           if ![Container::Locked, Container::Running].include?(c.state)
202             # container isn't locked or running, token shouldn't be used
203             return nil
204           end
205         end
206       end
207
208       # fast path: look up the token in the local database
209       auth = ApiClientAuthorization.
210              includes(:user, :api_client).
211              where('uuid=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token_uuid).
212              first
213       if auth && auth.user &&
214          (secret == auth.api_token ||
215           secret == OpenSSL::HMAC.hexdigest('sha1', auth.api_token, remote))
216         # found it
217         if token_uuid[0..4] != Rails.configuration.ClusterID
218           Rails.logger.debug "found cached remote token #{token_uuid} with secret #{secret} in local db"
219         end
220         return auth
221       end
222
223       upstream_cluster_id = token_uuid[0..4]
224       if upstream_cluster_id == Rails.configuration.ClusterID
225         # Token is supposedly issued by local cluster, but if the
226         # token were valid, we would have been found in the database
227         # in the above query.
228         return nil
229       elsif upstream_cluster_id.length != 5
230         # malformed
231         return nil
232       end
233
234     else
235       # token is not a 'v2' token. It could be just the secret part
236       # ("v1 token") -- or it could be an OpenIDConnect access token,
237       # in which case either (a) the controller will have inserted a
238       # row with api_token = hmac(systemroottoken,oidctoken) before
239       # forwarding it, or (b) we'll have done that ourselves, or (c)
240       # we'll need to ask LoginCluster to validate it for us below,
241       # and then insert a local row for a faster lookup next time.
242       hmac = OpenSSL::HMAC.hexdigest('sha256', Rails.configuration.SystemRootToken, token)
243       auth = ApiClientAuthorization.
244                includes(:user, :api_client).
245                where('api_token in (?, ?) and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token, hmac).
246                first
247       if auth && auth.user
248         return auth
249       elsif !Rails.configuration.Login.LoginCluster.blank? && Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
250         # An unrecognized non-v2 token might be an OIDC Access Token
251         # that can be verified by our login cluster in the code
252         # below. If so, we'll stuff the database with hmac instead of
253         # the real OIDC token.
254         upstream_cluster_id = Rails.configuration.Login.LoginCluster
255         stored_secret = hmac
256       else
257         return nil
258       end
259     end
260
261     # Invariant: upstream_cluster_id != Rails.configuration.ClusterID
262     #
263     # In other words the remaining code in this method decides
264     # whether to accept a token that was issued by a remote cluster
265     # when the token is absent or expired in our database.  To
266     # begin, we need to ask the cluster that issued the token to
267     # [re]validate it.
268     clnt = ApiClientAuthorization.make_http_client(uuid_prefix: upstream_cluster_id)
269
270     host = remote_host(uuid_prefix: upstream_cluster_id)
271     if !host
272       Rails.logger.warn "remote authentication rejected: no host for #{upstream_cluster_id.inspect}"
273       return nil
274     end
275     remote_url = URI::parse("https://#{host}/")
276     remote_query = {"remote" => Rails.configuration.ClusterID}
277     remote_headers = {"Authorization" => "Bearer #{token}"}
278
279     # First get the current token. This query is not limited by token scopes,
280     # and tells us the user's UUID via owner_uuid, so this gives us enough
281     # information to load a local user record from the database if one exists.
282     remote_token = nil
283     begin
284       remote_token = SafeJSON.load(
285         clnt.get_content(
286           remote_url.merge("arvados/v1/api_client_authorizations/current"),
287           remote_query, remote_headers,
288         ))
289       Rails.logger.debug "retrieved remote token #{remote_token.inspect}"
290       token_uuid = remote_token['uuid']
291       if !token_uuid.match(HasUuid::UUID_REGEX) || token_uuid[0..4] != upstream_cluster_id
292         raise "remote cluster #{upstream_cluster_id} returned invalid token uuid #{token_uuid.inspect}"
293       end
294     rescue HTTPClient::BadResponseError => e
295       if e.res.status_code >= 400 && e.res.status_code < 500
296         # Remote cluster does not accept this token.
297         return nil
298       end
299       # CurrentApiToken#call and ApplicationController#render_error will
300       # propagate the status code from the #http_status method, so define
301       # that here.
302       def e.http_status
303         self.res.status_code
304       end
305       raise
306     # TODO #20927: Catch network exceptions and assign a 5xx status to them so
307     # the client knows they're a temporary problem.
308     rescue => e
309       Rails.logger.warn "error getting remote token details for #{token.inspect}: #{e}"
310       return nil
311     end
312
313     # Next, load the token's user record from the database (might be nil).
314     remote_user_prefix, remote_user_suffix = remote_token['owner_uuid'].split('-', 2)
315     if anonymous_user_uuid.end_with?(remote_user_suffix)
316       # Special case: map the remote anonymous user to local anonymous user
317       remote_user_uuid = anonymous_user_uuid
318     else
319       remote_user_uuid = remote_token['owner_uuid']
320     end
321     user = User.find_by_uuid(remote_user_uuid)
322
323     # Next, try to load the remote user. If this succeeds, we'll use this
324     # information to update/create the local database record as necessary.
325     # If this fails for any reason, but we successfully loaded a user record
326     # from the database, we'll just rely on that information.
327     remote_user = nil
328     begin
329       remote_user = SafeJSON.load(
330         clnt.get_content(
331           remote_url.merge("arvados/v1/users/current"),
332           remote_query, remote_headers,
333         ))
334     rescue HTTPClient::BadResponseError => e
335       # If user is defined, we will use that alone for auth, see below.
336       if user.nil?
337         # See rationale in the previous BadResponseError rescue.
338         def e.http_status
339           self.res.status_code
340         end
341         raise
342       end
343     # TODO #20927: Catch network exceptions and assign a 5xx status to them so
344     # the client knows they're a temporary problem.
345     rescue => e
346       Rails.logger.warn "getting remote user with token #{token.inspect} failed: #{e}"
347     else
348       # Check the response is well formed.
349       if !remote_user.is_a?(Hash) || !remote_user['uuid'].is_a?(String)
350         Rails.logger.warn "malformed remote user=#{remote_user.inspect}"
351         remote_user = nil
352       # Clusters can only authenticate for their own users.
353       elsif remote_user_prefix != upstream_cluster_id
354         Rails.logger.warn "remote user rejected: claimed remote user #{remote_user_prefix} but token was issued by #{upstream_cluster_id}"
355         remote_user = nil
356       # Force our local copy of a remote root to have a static name
357       elsif system_user_uuid.end_with?(remote_user_suffix)
358         remote_user.update(
359           "first_name" => "root",
360           "last_name" => "from cluster #{remote_user_prefix}",
361         )
362       end
363     end
364
365     if user.nil? and remote_user.nil?
366       Rails.logger.warn "remote token #{token.inspect} rejected: cannot get owner #{remote_user_uuid} from database or remote cluster"
367       return nil
368     end
369
370     # Invariant:    remote_user_prefix == upstream_cluster_id
371     # therefore:    remote_user_prefix != Rails.configuration.ClusterID
372     # Add or update user and token in local database so we can
373     # validate subsequent requests faster.
374
375     act_as_system_user do
376       if remote_user && remote_user_uuid != anonymous_user_uuid
377         # Sync user record if we loaded a remote user.
378         user = User.update_remote_user remote_user
379       end
380
381       # If stored_secret is set, we save stored_secret in the database
382       # but return the real secret to the caller. This way, if we end
383       # up returning the auth record to the client, they see the same
384       # secret they supplied, instead of the HMAC we saved in the
385       # database.
386       stored_secret = stored_secret || secret
387
388       # We will accept this token (and avoid reloading the user
389       # record) for 'RemoteTokenRefresh' (default 5 minutes).
390       exp = [db_current_time + Rails.configuration.Login.RemoteTokenRefresh,
391              remote_token.andand['expires_at']].compact.min
392       scopes = remote_token.andand['scopes'] || ['all']
393       begin
394         retries ||= 0
395         auth = ApiClientAuthorization.find_or_create_by(uuid: token_uuid) do |auth|
396           auth.user = user
397           auth.api_token = stored_secret
398           auth.api_client_id = 0
399           auth.scopes = scopes
400           auth.expires_at = exp
401         end
402       rescue ActiveRecord::RecordNotUnique
403         Rails.logger.debug("cached remote token #{token_uuid} already exists, retrying...")
404         # Another request won the race (trying to find_or_create the
405         # same token UUID) ...and/or... there is an expired entry with
406         # the same secret but a different UUID (e.g., the token is an
407         # OIDC access token and [a] our database has an expired cached
408         # row that was not used above, and [b] the remote cluster had
409         # deleted its expired cached row so it assigned a new UUID).
410         #
411         # Delete any conflicting row if any. Retry twice (in case we
412         # hit both of those situations at once), then give up.
413         if (retries += 1) <= 2
414           ApiClientAuthorization.where('api_token=? and uuid<>?', stored_secret, token_uuid).delete_all
415           retry
416         else
417           Rails.logger.warn("cannot find or create cached remote token #{token_uuid}")
418           return nil
419         end
420       end
421       auth.update!(user: user,
422                    api_token: stored_secret,
423                    api_client_id: 0,
424                    scopes: scopes,
425                    expires_at: exp)
426       Rails.logger.debug "cached remote token #{token_uuid} with secret #{stored_secret} and scopes #{scopes} in local db"
427       auth.api_token = secret
428       return auth
429     end
430
431     return nil
432   end
433
434   def token
435     v2token
436   end
437
438   def v1token
439     api_token
440   end
441
442   def v2token
443     'v2/' + uuid + '/' + api_token
444   end
445
446   def salted_token(remote:)
447     if remote.nil?
448       token
449     end
450     'v2/' + uuid + '/' + OpenSSL::HMAC.hexdigest('sha1', api_token, remote)
451   end
452
453   protected
454
455   def clamp_token_expiration
456     if Rails.configuration.API.MaxTokenLifetime > 0
457       max_token_expiration = db_current_time + Rails.configuration.API.MaxTokenLifetime
458       if (self.new_record? || self.expires_at_changed?) && (self.expires_at.nil? || (self.expires_at > max_token_expiration && !current_user.andand.is_admin))
459         self.expires_at = max_token_expiration
460       end
461     end
462   end
463
464   def permission_to_create
465     current_user.andand.is_admin or (current_user.andand.id == self.user_id)
466   end
467
468   def permission_to_update
469     permission_to_create && !uuid_changed? &&
470       (current_user.andand.is_admin || !user_id_changed?)
471   end
472
473   def log_update
474     super unless (saved_changes.keys - UNLOGGED_CHANGES).empty?
475   end
476 end