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