Merge branch 'master' into 11453-federated-tokens
[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   extend CurrentApiClient
10
11   belongs_to :api_client
12   belongs_to :user
13   after_initialize :assign_random_api_token
14   serialize :scopes, Array
15
16   api_accessible :user, extend: :common do |t|
17     t.add :owner_uuid
18     t.add :user_id
19     t.add :api_client_id
20     t.add :api_token
21     t.add :created_by_ip_address
22     t.add :default_owner_uuid
23     t.add :expires_at
24     t.add :last_used_at
25     t.add :last_used_by_ip_address
26     t.add :scopes
27   end
28
29   UNLOGGED_CHANGES = ['last_used_at', 'last_used_by_ip_address', 'updated_at']
30
31   def assign_random_api_token
32     self.api_token ||= rand(2**256).to_s(36)
33   end
34
35   def owner_uuid
36     self.user.andand.uuid
37   end
38   def owner_uuid_was
39     self.user_id_changed? ? User.where(id: self.user_id_was).first.andand.uuid : self.user.andand.uuid
40   end
41   def owner_uuid_changed?
42     self.user_id_changed?
43   end
44
45   def modified_by_client_uuid
46     nil
47   end
48   def modified_by_client_uuid=(x) end
49
50   def modified_by_user_uuid
51     nil
52   end
53   def modified_by_user_uuid=(x) end
54
55   def modified_at
56     nil
57   end
58   def modified_at=(x) end
59
60   def scopes_allow?(req_s)
61     scopes.each do |scope|
62       return true if (scope == 'all') or (scope == req_s) or
63         ((scope.end_with? '/') and (req_s.start_with? scope))
64     end
65     false
66   end
67
68   def scopes_allow_request?(request)
69     method = request.request_method
70     if method == 'HEAD'
71       (scopes_allow?(['HEAD', request.path].join(' ')) ||
72        scopes_allow?(['GET', request.path].join(' ')))
73     else
74       scopes_allow?([method, request.path].join(' '))
75     end
76   end
77
78   def logged_attributes
79     super.except 'api_token'
80   end
81
82   def self.default_orders
83     ["#{table_name}.id desc"]
84   end
85
86   def self.remote_host(uuid_prefix:)
87     Rails.configuration.remote_hosts[uuid_prefix] ||
88       (Rails.configuration.remote_hosts_via_dns &&
89        uuid_prefix+".arvadosapi.com")
90   end
91
92   def self.validate(token:, remote:)
93     return nil if !token
94     remote ||= Rails.configuration.uuid_prefix
95
96     case token[0..2]
97     when 'v2/'
98       _, uuid, secret = token.split('/')
99       auth = ApiClientAuthorization.
100              includes(:user, :api_client).
101              where('uuid=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', uuid).
102              first
103       if auth && auth.user &&
104          (secret == auth.api_token ||
105           secret == OpenSSL::HMAC.hexdigest('sha1', auth.api_token, remote))
106         return auth
107       end
108
109       uuid_prefix = uuid[0..4]
110       if uuid_prefix == Rails.configuration.uuid_prefix
111         # If the token were valid, we would have validated it above
112         return nil
113       elsif uuid_prefix.length != 5
114         # malformed
115         return nil
116       end
117
118       host = remote_host(uuid_prefix: uuid_prefix)
119       if !host
120         Rails.logger.warn "remote authentication rejected: no host for #{uuid_prefix.inspect}"
121         return nil
122       end
123
124       # Token was issued by a different cluster. If it's expired or
125       # missing in our database, ask the originating cluster to
126       # [re]validate it.
127       begin
128         clnt = HTTPClient.new
129         remote_user = SafeJSON.load(
130           clnt.get_content('https://' + host + '/arvados/v1/users/current',
131                            {'remote' => Rails.configuration.uuid_prefix},
132                            {'Authorization' => 'Bearer ' + token}))
133       rescue => e
134         logger.warn "remote authentication with token #{token.inspect} failed: #{e}"
135         STDERR.puts e.backtrace
136         return nil
137       end
138       if !remote_user.is_a?(Hash) || !remote_user[:uuid].is_a?(String) || remote_user[:uuid][0..4] != uuid[0..4]
139         logger.warn "remote authentication rejected: remote_user=#{remote_user.inspect}"
140         return nil
141       end
142       act_as_system_user do
143         # Add/update user and token in our database so we can
144         # validate subsequent requests faster.
145
146         user = User.find_or_create_by(uuid: remote_user[:uuid])
147
148         updates = {}
149         [:first_name, :last_name, :email, :prefs].each do |attr|
150           updates[attr] = remote_user[attr]
151         end
152
153         if Rails.configuration.new_users_are_active
154           # Update is_active to whatever it is at the remote end
155           updates[:is_active] = remote_user[:is_active]
156         elsif !updates[:is_active]
157           # Remote user is inactive; our mirror should be, too.
158           updates[:is_active] = false
159         end
160
161         user.update_attributes!(updates)
162
163         auth = ApiClientAuthorization.find_or_create_by(uuid: uuid)
164         auth.user = user
165         auth.api_token = token
166         auth.api_client_id = 0
167         auth.save!
168
169         # Accept this token (and don't reload the user record) for
170         # 5 minutes. TODO: Request the actual api_client_auth
171         # record from the remote server in case it wants the token
172         # to expire sooner.
173         auth.update_attributes!(expires_at: Time.now + 5.minutes)
174       end
175       return auth
176     else
177       auth = ApiClientAuthorization.
178              includes(:user, :api_client).
179              where('api_token=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token).
180              first
181       if auth && auth.user
182         return auth
183       end
184     end
185     return nil
186   end
187
188   protected
189
190   def permission_to_create
191     current_user.andand.is_admin or (current_user.andand.id == self.user_id)
192   end
193
194   def permission_to_update
195     (permission_to_create and
196      not uuid_changed? and
197      not user_id_changed? and
198      not owner_uuid_changed?)
199   end
200
201   def log_update
202     super unless (changed - UNLOGGED_CHANGES).empty?
203   end
204 end