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