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