4253: Use same repo name logic whether or not user already existed before setup.
[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
312     unless current_user == self or current_user.can? write: owner_uuid
313       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}"
314       errors.add :owner_uuid, "cannot be changed without write permission on new owner"
315       raise PermissionDeniedError
316     end
317
318     true
319   end
320
321   def ensure_permission_to_save
322     unless (new_record? ? permission_to_create : permission_to_update)
323       raise PermissionDeniedError
324     end
325   end
326
327   def permission_to_create
328     current_user.andand.is_active
329   end
330
331   def permission_to_update
332     if !current_user
333       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
334       return false
335     end
336     if !current_user.is_active
337       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
338       return false
339     end
340     return true if current_user.is_admin
341     if self.uuid_changed?
342       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
343       return false
344     end
345     return true
346   end
347
348   def ensure_permission_to_destroy
349     raise PermissionDeniedError unless permission_to_destroy
350   end
351
352   def permission_to_destroy
353     permission_to_update
354   end
355
356   def maybe_update_modified_by_fields
357     update_modified_by_fields if self.changed? or self.new_record?
358     true
359   end
360
361   def update_modified_by_fields
362     current_time = db_current_time
363     self.updated_at = current_time
364     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
365     self.modified_at = current_time
366     self.modified_by_user_uuid = current_user ? current_user.uuid : nil
367     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
368     true
369   end
370
371   def self.has_symbols? x
372     if x.is_a? Hash
373       x.each do |k,v|
374         return true if has_symbols?(k) or has_symbols?(v)
375       end
376       false
377     elsif x.is_a? Array
378       x.each do |k|
379         return true if has_symbols?(k)
380       end
381       false
382     else
383       (x.class == Symbol)
384     end
385   end
386
387   def self.recursive_stringify x
388     if x.is_a? Hash
389       Hash[x.collect do |k,v|
390              [recursive_stringify(k), recursive_stringify(v)]
391            end]
392     elsif x.is_a? Array
393       x.collect do |k|
394         recursive_stringify k
395       end
396     elsif x.is_a? Symbol
397       x.to_s
398     else
399       x
400     end
401   end
402
403   def ensure_serialized_attribute_type
404     # Specifying a type in the "serialize" declaration causes rails to
405     # raise an exception if a different data type is retrieved from
406     # the database during load().  The validation preventing such
407     # crash-inducing records from being inserted in the database in
408     # the first place seems to have been left as an exercise to the
409     # developer.
410     self.class.serialized_attributes.each do |colname, attr|
411       if attr.object_class
412         if self.attributes[colname].class != attr.object_class
413           self.errors.add colname.to_sym, "must be a #{attr.object_class.to_s}, not a #{self.attributes[colname].class.to_s}"
414         elsif self.class.has_symbols? attributes[colname]
415           self.errors.add colname.to_sym, "must not contain symbols: #{attributes[colname].inspect}"
416         end
417       end
418     end
419   end
420
421   def convert_serialized_symbols_to_strings
422     # ensure_serialized_attribute_type should prevent symbols from
423     # getting into the database in the first place. If someone managed
424     # to get them into the database (perhaps using an older version)
425     # we'll convert symbols to strings when loading from the
426     # database. (Otherwise, loading and saving an object with existing
427     # symbols in a serialized field will crash.)
428     self.class.serialized_attributes.each do |colname, attr|
429       if self.class.has_symbols? attributes[colname]
430         attributes[colname] = self.class.recursive_stringify attributes[colname]
431         self.send(colname + '=',
432                   self.class.recursive_stringify(attributes[colname]))
433       end
434     end
435   end
436
437   def foreign_key_attributes
438     attributes.keys.select { |a| a.match /_uuid$/ }
439   end
440
441   def skip_uuid_read_permission_check
442     %w(modified_by_client_uuid)
443   end
444
445   def skip_uuid_existence_check
446     []
447   end
448
449   def normalize_collection_uuids
450     foreign_key_attributes.each do |attr|
451       attr_value = send attr
452       if attr_value.is_a? String and
453           attr_value.match /^[0-9a-f]{32,}(\+[@\w]+)*$/
454         begin
455           send "#{attr}=", Collection.normalize_uuid(attr_value)
456         rescue
457           # TODO: abort instead of silently accepting unnormalizable value?
458         end
459       end
460     end
461   end
462
463   @@prefixes_hash = nil
464   def self.uuid_prefixes
465     unless @@prefixes_hash
466       @@prefixes_hash = {}
467       Rails.application.eager_load!
468       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
469         if k.respond_to?(:uuid_prefix)
470           @@prefixes_hash[k.uuid_prefix] = k
471         end
472       end
473     end
474     @@prefixes_hash
475   end
476
477   def self.uuid_like_pattern
478     "_____-#{uuid_prefix}-_______________"
479   end
480
481   def self.uuid_regex
482     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
483   end
484
485   def ensure_valid_uuids
486     specials = [system_user_uuid]
487
488     foreign_key_attributes.each do |attr|
489       if new_record? or send (attr + "_changed?")
490         next if skip_uuid_existence_check.include? attr
491         attr_value = send attr
492         next if specials.include? attr_value
493         if attr_value
494           if (r = ArvadosModel::resource_class_for_uuid attr_value)
495             unless skip_uuid_read_permission_check.include? attr
496               r = r.readable_by(current_user)
497             end
498             if r.where(uuid: attr_value).count == 0
499               errors.add(attr, "'#{attr_value}' not found")
500             end
501           end
502         end
503       end
504     end
505   end
506
507   class Email
508     def self.kind
509       "email"
510     end
511
512     def kind
513       self.class.kind
514     end
515
516     def self.readable_by (*u)
517       self
518     end
519
520     def self.where (u)
521       [{:uuid => u[:uuid]}]
522     end
523   end
524
525   def self.resource_class_for_uuid(uuid)
526     if uuid.is_a? ArvadosModel
527       return uuid.class
528     end
529     unless uuid.is_a? String
530       return nil
531     end
532     resource_class = nil
533
534     uuid.match HasUuid::UUID_REGEX do |re|
535       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
536     end
537
538     if uuid.match /.+@.+/
539       return Email
540     end
541
542     nil
543   end
544
545   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
546   # an object in any class.
547   def self.find_by_uuid uuid
548     if self == ArvadosModel
549       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
550       # delegate to the appropriate subclass based on the given uuid.
551       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
552     else
553       super
554     end
555   end
556
557   def log_start_state
558     @old_attributes = Marshal.load(Marshal.dump(attributes))
559     @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
560   end
561
562   def log_change(event_type)
563     log = Log.new(event_type: event_type).fill_object(self)
564     yield log
565     log.save!
566     log_start_state
567   end
568
569   def log_create
570     log_change('create') do |log|
571       log.fill_properties('old', nil, nil)
572       log.update_to self
573     end
574   end
575
576   def log_update
577     log_change('update') do |log|
578       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
579       log.update_to self
580     end
581   end
582
583   def log_destroy
584     log_change('destroy') do |log|
585       log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
586       log.update_to nil
587     end
588   end
589 end