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