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