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