3859: Implement Job lock method on api server. This takes a queued job and
[arvados.git] / services / api / app / models / arvados_model.rb
1 require 'has_uuid'
2
3 class ArvadosModel < ActiveRecord::Base
4   self.abstract_class = true
5
6   include CurrentApiClient      # current_user, current_api_client, etc.
7
8   attr_protected :created_at
9   attr_protected :modified_by_user_uuid
10   attr_protected :modified_by_client_uuid
11   attr_protected :modified_at
12   after_initialize :log_start_state
13   before_save :ensure_permission_to_save
14   before_save :ensure_owner_uuid_is_permitted
15   before_save :ensure_ownership_path_leads_to_user
16   before_destroy :ensure_owner_uuid_is_permitted
17   before_destroy :ensure_permission_to_destroy
18   before_create :update_modified_by_fields
19   before_update :maybe_update_modified_by_fields
20   after_create :log_create
21   after_update :log_update
22   after_destroy :log_destroy
23   after_find :convert_serialized_symbols_to_strings
24   before_validation :normalize_collection_uuids
25   validate :ensure_serialized_attribute_type
26   validate :ensure_valid_uuids
27
28   # Note: This only returns permission links. It does not account for
29   # permissions obtained via user.is_admin or
30   # user.uuid==object.owner_uuid.
31   has_many :permissions, :foreign_key => :head_uuid, :class_name => 'Link', :primary_key => :uuid, :conditions => "link_class = 'permission'"
32
33   class PermissionDeniedError < StandardError
34     def http_status
35       403
36     end
37   end
38
39   class UnauthorizedError < StandardError
40     def http_status
41       401
42     end
43   end
44
45   class ConflictError < StandardError
46     def http_status
47       409
48     end
49   end
50
51
52   def self.kind_class(kind)
53     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
54   end
55
56   def href
57     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
58   end
59
60   def self.searchable_columns operator
61     textonly_operator = !operator.match(/[<=>]/)
62     self.columns.select do |col|
63       case col.type
64       when :string, :text
65         true
66       when :datetime, :integer, :boolean
67         !textonly_operator
68       else
69         false
70       end
71     end.map(&:name)
72   end
73
74   def self.attribute_column attr
75     self.columns.select { |col| col.name == attr.to_s }.first
76   end
77
78   def self.attributes_required_columns
79     # This method returns a hash.  Each key is the name of an API attribute,
80     # and it's mapped to a list of database columns that must be fetched
81     # to generate that attribute.
82     # This implementation generates a simple map of attributes to
83     # matching column names.  Subclasses can override this method
84     # to specify that method-backed API attributes need to fetch
85     # specific columns from the database.
86     all_columns = columns.map(&:name)
87     api_column_map = Hash.new { |hash, key| hash[key] = [] }
88     methods.grep(/^api_accessible_\w+$/).each do |method_name|
89       next if method_name == :api_accessible_attributes
90       send(method_name).each_pair do |api_attr_name, col_name|
91         col_name = col_name.to_s
92         if all_columns.include?(col_name)
93           api_column_map[api_attr_name.to_s] |= [col_name]
94         end
95       end
96     end
97     api_column_map
98   end
99
100   # If current user can manage the object, return an array of uuids of
101   # users and groups that have permission to write the object. The
102   # first two elements are always [self.owner_uuid, current user's
103   # uuid].
104   #
105   # If current user can write but not manage the object, return
106   # [self.owner_uuid, current user's uuid].
107   #
108   # If current user cannot write this object, just return
109   # [self.owner_uuid].
110   def writable_by
111     unless (owner_uuid == current_user.uuid or
112             current_user.is_admin or
113             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
114       if current_user.groups_i_can(:write).index(uuid)
115         return [owner_uuid, current_user.uuid]
116       else
117         return [owner_uuid]
118       end
119     end
120     [owner_uuid, current_user.uuid] + permissions.collect do |p|
121       if ['can_write', 'can_manage'].index p.name
122         p.tail_uuid
123       end
124     end.compact.uniq
125   end
126
127   # Return a query with read permissions restricted to the union of of the
128   # permissions of the members of users_list, i.e. if something is readable by
129   # any user in users_list, it will be readable in the query returned by this
130   # function.
131   def self.readable_by(*users_list)
132     # Get rid of troublesome nils
133     users_list.compact!
134
135     # Load optional keyword arguments, if they exist.
136     if users_list.last.is_a? Hash
137       kwargs = users_list.pop
138     else
139       kwargs = {}
140     end
141
142     # Check if any of the users are admin.  If so, we're done.
143     if users_list.select { |u| u.is_admin }.any?
144       return self
145     end
146
147     # Collect the uuids for each user and any groups readable by each user.
148     user_uuids = users_list.map { |u| u.uuid }
149     uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
150     sql_conds = []
151     sql_params = []
152     sql_table = kwargs.fetch(:table_name, table_name)
153     or_object_uuid = ''
154
155     # This row is owned by a member of users_list, or owned by a group
156     # readable by a member of users_list
157     # or
158     # This row uuid is the uuid of a member of users_list
159     # or
160     # A permission link exists ('write' and 'manage' implicitly include
161     # 'read') from a member of users_list, or a group readable by users_list,
162     # to this row, or to the owner of this row (see join() below).
163     sql_conds += ["#{sql_table}.uuid in (?)"]
164     sql_params += [user_uuids]
165
166     if uuid_list.any?
167       sql_conds += ["#{sql_table}.owner_uuid in (?)"]
168       sql_params += [uuid_list]
169
170       sanitized_uuid_list = uuid_list.
171         collect { |uuid| sanitize(uuid) }.join(', ')
172       permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))"
173       sql_conds += ["#{sql_table}.uuid IN #{permitted_uuids}"]
174     end
175
176     if sql_table == "links" and users_list.any?
177       # This row is a 'permission' or 'resources' link class
178       # The uuid for a member of users_list is referenced in either the head
179       # or tail of the link
180       sql_conds += ["(#{sql_table}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND (#{sql_table}.head_uuid IN (?) OR #{sql_table}.tail_uuid IN (?)))"]
181       sql_params += [user_uuids, user_uuids]
182     end
183
184     if sql_table == "logs" and users_list.any?
185       # Link head points to the object described by this row
186       sql_conds += ["#{sql_table}.object_uuid IN #{permitted_uuids}"]
187
188       # This object described by this row is owned by this user, or owned by a group readable by this user
189       sql_conds += ["#{sql_table}.object_owner_uuid in (?)"]
190       sql_params += [uuid_list]
191     end
192
193     # Link head points to this row, or to the owner of this row (the
194     # thing to be read)
195     #
196     # Link tail originates from this user, or a group that is readable
197     # by this user (the identity with authorization to read)
198     #
199     # Link class is 'permission' ('write' and 'manage' implicitly
200     # include 'read')
201     where(sql_conds.join(' OR '), *sql_params)
202   end
203
204   def logged_attributes
205     attributes
206   end
207
208   protected
209
210   def ensure_ownership_path_leads_to_user
211     if new_record? or owner_uuid_changed?
212       uuid_in_path = {owner_uuid => true, uuid => true}
213       x = owner_uuid
214       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
215         begin
216           if x == uuid
217             # Test for cycles with the new version, not the DB contents
218             x = owner_uuid
219           elsif !owner_class.respond_to? :find_by_uuid
220             raise ActiveRecord::RecordNotFound.new
221           else
222             x = owner_class.find_by_uuid(x).owner_uuid
223           end
224         rescue ActiveRecord::RecordNotFound => e
225           errors.add :owner_uuid, "is not owned by any user: #{e}"
226           return false
227         end
228         if uuid_in_path[x]
229           if x == owner_uuid
230             errors.add :owner_uuid, "would create an ownership cycle"
231           else
232             errors.add :owner_uuid, "has an ownership cycle"
233           end
234           return false
235         end
236         uuid_in_path[x] = true
237       end
238     end
239     true
240   end
241
242   def ensure_owner_uuid_is_permitted
243     raise PermissionDeniedError if !current_user
244
245     if new_record? and respond_to? :owner_uuid=
246       self.owner_uuid ||= current_user.uuid
247     end
248
249     if self.owner_uuid.nil?
250       errors.add :owner_uuid, "cannot be nil"
251       raise PermissionDeniedError
252     end
253
254     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
255     unless rsc_class == User or rsc_class == Group
256       errors.add :owner_uuid, "must be set to User or Group"
257       raise PermissionDeniedError
258     end
259
260     # Verify "write" permission on old owner
261     # default fail unless one of:
262     # owner_uuid did not change
263     # previous owner_uuid is nil
264     # current user is the old owner
265     # current user is this object
266     # current user can_write old owner
267     unless !owner_uuid_changed? or
268         owner_uuid_was.nil? or
269         current_user.uuid == self.owner_uuid_was or
270         current_user.uuid == self.uuid or
271         current_user.can? write: self.owner_uuid_was
272       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write old owner_uuid #{owner_uuid_was}"
273       errors.add :owner_uuid, "cannot be changed without write permission on old owner"
274       raise PermissionDeniedError
275     end
276
277     # Verify "write" permission on new owner
278     # default fail unless one of:
279     # current_user is this object
280     # current user can_write new owner
281     unless current_user == self or current_user.can? write: owner_uuid
282       logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{uuid} but does not have permission to write new owner_uuid #{owner_uuid}"
283       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
284       raise PermissionDeniedError
285     end
286
287     true
288   end
289
290   def ensure_permission_to_save
291     unless (new_record? ? permission_to_create : permission_to_update)
292       raise PermissionDeniedError
293     end
294   end
295
296   def permission_to_create
297     current_user.andand.is_active
298   end
299
300   def permission_to_update
301     if !current_user
302       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
303       return false
304     end
305     if !current_user.is_active
306       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
307       return false
308     end
309     return true if current_user.is_admin
310     if self.uuid_changed?
311       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
312       return false
313     end
314     return true
315   end
316
317   def ensure_permission_to_destroy
318     raise PermissionDeniedError unless permission_to_destroy
319   end
320
321   def permission_to_destroy
322     permission_to_update
323   end
324
325   def maybe_update_modified_by_fields
326     update_modified_by_fields if self.changed? or self.new_record?
327     true
328   end
329
330   def update_modified_by_fields
331     self.updated_at = Time.now
332     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
333     self.modified_at = Time.now
334     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
335     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
336     true
337   end
338
339   def self.has_symbols? x
340     if x.is_a? Hash
341       x.each do |k,v|
342         return true if has_symbols?(k) or has_symbols?(v)
343       end
344       false
345     elsif x.is_a? Array
346       x.each do |k|
347         return true if has_symbols?(k)
348       end
349       false
350     else
351       (x.class == Symbol)
352     end
353   end
354
355   def self.recursive_stringify x
356     if x.is_a? Hash
357       Hash[x.collect do |k,v|
358              [recursive_stringify(k), recursive_stringify(v)]
359            end]
360     elsif x.is_a? Array
361       x.collect do |k|
362         recursive_stringify k
363       end
364     elsif x.is_a? Symbol
365       x.to_s
366     else
367       x
368     end
369   end
370
371   def ensure_serialized_attribute_type
372     # Specifying a type in the "serialize" declaration causes rails to
373     # raise an exception if a different data type is retrieved from
374     # the database during load().  The validation preventing such
375     # crash-inducing records from being inserted in the database in
376     # the first place seems to have been left as an exercise to the
377     # developer.
378     self.class.serialized_attributes.each do |colname, attr|
379       if attr.object_class
380         if self.attributes[colname].class != attr.object_class
381           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
382         elsif self.class.has_symbols? attributes[colname]
383           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
384         end
385       end
386     end
387   end
388
389   def convert_serialized_symbols_to_strings
390     # ensure_serialized_attribute_type should prevent symbols from
391     # getting into the database in the first place. If someone managed
392     # to get them into the database (perhaps using an older version)
393     # we'll convert symbols to strings when loading from the
394     # database. (Otherwise, loading and saving an object with existing
395     # symbols in a serialized field will crash.)
396     self.class.serialized_attributes.each do |colname, attr|
397       if self.class.has_symbols? attributes[colname]
398         attributes[colname] = self.class.recursive_stringify attributes[colname]
399         self.send(colname + '=',
400                   self.class.recursive_stringify(attributes[colname]))
401       end
402     end
403   end
404
405   def foreign_key_attributes
406     attributes.keys.select { |a| a.match /_uuid$/ }
407   end
408
409   def skip_uuid_read_permission_check
410     %w(modified_by_client_uuid)
411   end
412
413   def skip_uuid_existence_check
414     []
415   end
416
417   def normalize_collection_uuids
418     foreign_key_attributes.each do |attr|
419       attr_value = send attr
420       if attr_value.is_a? String and
421           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
422         begin
423           send "#{attr}=", Collection.normalize_uuid(attr_value)
424         rescue
425           # TODO: abort instead of silently accepting unnormalizable value?
426         end
427       end
428     end
429   end
430
431   @@prefixes_hash = nil
432   def self.uuid_prefixes
433     unless @@prefixes_hash
434       @@prefixes_hash = {}
435       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
436         if k.respond_to?(:uuid_prefix)
437           @@prefixes_hash[k.uuid_prefix] = k
438         end
439       end
440     end
441     @@prefixes_hash
442   end
443
444   def self.uuid_like_pattern
445     "_____-#{uuid_prefix}-_______________"
446   end
447
448   def ensure_valid_uuids
449     specials = [system_user_uuid]
450
451     foreign_key_attributes.each do |attr|
452       if new_record? or send (attr + "_changed?")
453         next if skip_uuid_existence_check.include? attr
454         attr_value = send attr
455         next if specials.include? attr_value
456         if attr_value
457           if (r = ArvadosModel::resource_class_for_uuid attr_value)
458             unless skip_uuid_read_permission_check.include? attr
459               r = r.readable_by(current_user)
460             end
461             if r.where(uuid: attr_value).count == 0
462               errors.add(attr, "'#{attr_value}' not found")
463             end
464           end
465         end
466       end
467     end
468   end
469
470   class Email
471     def self.kind
472       "email"
473     end
474
475     def kind
476       self.class.kind
477     end
478
479     def self.readable_by (*u)
480       self
481     end
482
483     def self.where (u)
484       [{:uuid => u[:uuid]}]
485     end
486   end
487
488   def self.resource_class_for_uuid(uuid)
489     if uuid.is_a? ArvadosModel
490       return uuid.class
491     end
492     unless uuid.is_a? String
493       return nil
494     end
495     resource_class = nil
496
497     Rails.application.eager_load!
498     uuid.match HasUuid::UUID_REGEX do |re|
499       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
500     end
501
502     if uuid.match /.+@.+/
503       return Email
504     end
505
506     nil
507   end
508
509   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
510   # an object in any class.
511   def self.find_by_uuid uuid
512     if self == ArvadosModel
513       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
514       # delegate to the appropriate subclass based on the given uuid.
515       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
516     else
517       super
518     end
519   end
520
521   def log_start_state
522     @old_etag = etag
523     @old_attributes = logged_attributes
524   end
525
526   def log_change(event_type)
527     log = Log.new(event_type: event_type).fill_object(self)
528     yield log
529     log.save!
530     connection.execute "NOTIFY logs, '#{log.id}'"
531     log_start_state
532   end
533
534   def log_create
535     log_change('create') do |log|
536       log.fill_properties('old', nil, nil)
537       log.update_to self
538     end
539   end
540
541   def log_update
542     log_change('update') do |log|
543       log.fill_properties('old', @old_etag, @old_attributes)
544       log.update_to self
545     end
546   end
547
548   def log_destroy
549     log_change('destroy') do |log|
550       log.fill_properties('old', @old_etag, @old_attributes)
551       log.update_to nil
552     end
553   end
554 end