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