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