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