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