Add 'build/' from commit '555b039609a3c8700c27767c255fdfe00eb42063'
[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       false
395     elsif x.is_a? Array
396       x.each do |k|
397         return true if has_symbols?(k)
398       end
399       false
400     else
401       (x.class == Symbol)
402     end
403   end
404
405   def self.recursive_stringify x
406     if x.is_a? Hash
407       Hash[x.collect do |k,v|
408              [recursive_stringify(k), recursive_stringify(v)]
409            end]
410     elsif x.is_a? Array
411       x.collect do |k|
412         recursive_stringify k
413       end
414     elsif x.is_a? Symbol
415       x.to_s
416     else
417       x
418     end
419   end
420
421   def ensure_serialized_attribute_type
422     # Specifying a type in the "serialize" declaration causes rails to
423     # raise an exception if a different data type is retrieved from
424     # the database during load().  The validation preventing such
425     # crash-inducing records from being inserted in the database in
426     # the first place seems to have been left as an exercise to the
427     # developer.
428     self.class.serialized_attributes.each do |colname, attr|
429       if attr.object_class
430         if self.attributes[colname].class != attr.object_class
431           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
432         elsif self.class.has_symbols? attributes[colname]
433           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
434         end
435       end
436     end
437   end
438
439   def convert_serialized_symbols_to_strings
440     # ensure_serialized_attribute_type should prevent symbols from
441     # getting into the database in the first place. If someone managed
442     # to get them into the database (perhaps using an older version)
443     # we'll convert symbols to strings when loading from the
444     # database. (Otherwise, loading and saving an object with existing
445     # symbols in a serialized field will crash.)
446     self.class.serialized_attributes.each do |colname, attr|
447       if self.class.has_symbols? attributes[colname]
448         attributes[colname] = self.class.recursive_stringify attributes[colname]
449         self.send(colname + '=',
450                   self.class.recursive_stringify(attributes[colname]))
451       end
452     end
453   end
454
455   def foreign_key_attributes
456     attributes.keys.select { |a| a.match /_uuid$/ }
457   end
458
459   def skip_uuid_read_permission_check
460     %w(modified_by_client_uuid)
461   end
462
463   def skip_uuid_existence_check
464     []
465   end
466
467   def normalize_collection_uuids
468     foreign_key_attributes.each do |attr|
469       attr_value = send attr
470       if attr_value.is_a? String and
471           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
472         begin
473           send "#{attr}=", Collection.normalize_uuid(attr_value)
474         rescue
475           # TODO: abort instead of silently accepting unnormalizable value?
476         end
477       end
478     end
479   end
480
481   @@prefixes_hash = nil
482   def self.uuid_prefixes
483     unless @@prefixes_hash
484       @@prefixes_hash = {}
485       Rails.application.eager_load!
486       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
487         if k.respond_to?(:uuid_prefix)
488           @@prefixes_hash[k.uuid_prefix] = k
489         end
490       end
491     end
492     @@prefixes_hash
493   end
494
495   def self.uuid_like_pattern
496     "_____-#{uuid_prefix}-_______________"
497   end
498
499   def self.uuid_regex
500     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
501   end
502
503   def ensure_valid_uuids
504     specials = [system_user_uuid]
505
506     foreign_key_attributes.each do |attr|
507       if new_record? or send (attr + "_changed?")
508         next if skip_uuid_existence_check.include? attr
509         attr_value = send attr
510         next if specials.include? attr_value
511         if attr_value
512           if (r = ArvadosModel::resource_class_for_uuid attr_value)
513             unless skip_uuid_read_permission_check.include? attr
514               r = r.readable_by(current_user)
515             end
516             if r.where(uuid: attr_value).count == 0
517               errors.add(attr, "'#{attr_value}' not found")
518             end
519           end
520         end
521       end
522     end
523   end
524
525   class Email
526     def self.kind
527       "email"
528     end
529
530     def kind
531       self.class.kind
532     end
533
534     def self.readable_by (*u)
535       self
536     end
537
538     def self.where (u)
539       [{:uuid => u[:uuid]}]
540     end
541   end
542
543   def self.resource_class_for_uuid(uuid)
544     if uuid.is_a? ArvadosModel
545       return uuid.class
546     end
547     unless uuid.is_a? String
548       return nil
549     end
550     resource_class = nil
551
552     uuid.match HasUuid::UUID_REGEX do |re|
553       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
554     end
555
556     if uuid.match /.+@.+/
557       return Email
558     end
559
560     nil
561   end
562
563   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
564   # an object in any class.
565   def self.find_by_uuid uuid
566     if self == ArvadosModel
567       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
568       # delegate to the appropriate subclass based on the given uuid.
569       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
570     else
571       super
572     end
573   end
574
575   def log_start_state
576     @old_attributes = Marshal.load(Marshal.dump(attributes))
577     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
578   end
579
580   def log_change(event_type)
581     log = Log.new(event_type: event_type).fill_object(self)
582     yield log
583     log.save!
584     log_start_state
585   end
586
587   def log_create
588     log_change('create') do |log|
589       log.fill_properties('old', nil, nil)
590       log.update_to self
591     end
592   end
593
594   def log_update
595     log_change('update') do |log|
596       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
597       log.update_to self
598     end
599   end
600
601   def log_destroy
602     log_change('destroy') do |log|
603       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
604       log.update_to nil
605     end
606   end
607 end