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