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