16007: Remember to set trash_at in trashed_groups.
[arvados.git] / services / api / app / models / arvados_model.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'arvados_model_updates'
6 require 'has_uuid'
7 require 'record_filters'
8 require 'serializers'
9 require 'request_error'
10
11 class ArvadosModel < ApplicationRecord
12   self.abstract_class = true
13
14   include ArvadosModelUpdates
15   include CurrentApiClient      # current_user, current_api_client, etc.
16   include DbCurrentTime
17   extend RecordFilters
18
19   after_initialize :log_start_state
20   before_save :ensure_permission_to_save
21   before_save :ensure_owner_uuid_is_permitted
22   before_save :ensure_ownership_path_leads_to_user
23   before_destroy :ensure_owner_uuid_is_permitted
24   before_destroy :ensure_permission_to_destroy
25   before_create :update_modified_by_fields
26   before_update :maybe_update_modified_by_fields
27   after_create :log_create
28   after_update :log_update
29   after_destroy :log_destroy
30   before_validation :normalize_collection_uuids
31   before_validation :set_default_owner
32   validate :ensure_valid_uuids
33
34   # Note: This only returns permission links. It does not account for
35   # permissions obtained via user.is_admin or
36   # user.uuid==object.owner_uuid.
37   has_many(:permissions,
38            ->{where(link_class: 'permission')},
39            foreign_key: :head_uuid,
40            class_name: 'Link',
41            primary_key: :uuid)
42
43   # If async is true at create or update, permission graph
44   # update is deferred allowing making multiple calls without the performance
45   # penalty.
46   attr_accessor :async_permissions_update
47
48   # Ignore listed attributes on mass assignments
49   def self.protected_attributes
50     []
51   end
52
53   class PermissionDeniedError < RequestError
54     def http_status
55       403
56     end
57   end
58
59   class AlreadyLockedError < RequestError
60     def http_status
61       422
62     end
63   end
64
65   class LockFailedError < RequestError
66     def http_status
67       422
68     end
69   end
70
71   class InvalidStateTransitionError < RequestError
72     def http_status
73       422
74     end
75   end
76
77   class UnauthorizedError < RequestError
78     def http_status
79       401
80     end
81   end
82
83   class UnresolvableContainerError < RequestError
84     def http_status
85       422
86     end
87   end
88
89   def self.kind_class(kind)
90     kind.match(/^arvados\#(.+)$/)[1].classify.safe_constantize rescue nil
91   end
92
93   def href
94     "#{current_api_base}/#{self.class.to_s.pluralize.underscore}/#{self.uuid}"
95   end
96
97   def self.permit_attribute_params raw_params
98     # strong_parameters does not provide security: permissions are
99     # implemented with before_save hooks.
100     #
101     # The following permit! is necessary even with
102     # "ActionController::Parameters.permit_all_parameters = true",
103     # because permit_all does not permit nested attributes.
104     raw_params ||= {}
105
106     if raw_params
107       raw_params = raw_params.to_hash
108       raw_params.delete_if { |k, _| self.protected_attributes.include? k }
109       serialized_attributes.each do |colname, coder|
110         param = raw_params[colname.to_sym]
111         if param.nil?
112           # ok
113         elsif !param.is_a?(coder.object_class)
114           raise ArgumentError.new("#{colname} parameter must be #{coder.object_class}, not #{param.class}")
115         elsif has_nonstring_keys?(param)
116           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
117         end
118       end
119       # Check JSONB columns that aren't listed on serialized_attributes
120       columns.select{|c| c.type == :jsonb}.collect{|j| j.name}.each do |colname|
121         if serialized_attributes.include? colname || raw_params[colname.to_sym].nil?
122           next
123         end
124         if has_nonstring_keys?(raw_params[colname.to_sym])
125           raise ArgumentError.new("#{colname} parameter cannot have non-string hash keys")
126         end
127       end
128     end
129     ActionController::Parameters.new(raw_params).permit!
130   end
131
132   def initialize raw_params={}, *args
133     super(self.class.permit_attribute_params(raw_params), *args)
134   end
135
136   # Reload "old attributes" for logging, too.
137   def reload(*args)
138     super
139     log_start_state
140   end
141
142   def self.create raw_params={}, *args
143     super(permit_attribute_params(raw_params), *args)
144   end
145
146   def update_attributes raw_params={}, *args
147     super(self.class.permit_attribute_params(raw_params), *args)
148   end
149
150   def self.selectable_attributes(template=:user)
151     # Return an array of attribute name strings that can be selected
152     # in the given template.
153     api_accessible_attributes(template).map { |attr_spec| attr_spec.first.to_s }
154   end
155
156   def self.searchable_columns operator
157     textonly_operator = !operator.match(/[<=>]/)
158     self.columns.select do |col|
159       case col.type
160       when :string, :text
161         true
162       when :datetime, :integer, :boolean
163         !textonly_operator
164       else
165         false
166       end
167     end.map(&:name)
168   end
169
170   def self.attribute_column attr
171     self.columns.select { |col| col.name == attr.to_s }.first
172   end
173
174   def self.attributes_required_columns
175     # This method returns a hash.  Each key is the name of an API attribute,
176     # and it's mapped to a list of database columns that must be fetched
177     # to generate that attribute.
178     # This implementation generates a simple map of attributes to
179     # matching column names.  Subclasses can override this method
180     # to specify that method-backed API attributes need to fetch
181     # specific columns from the database.
182     all_columns = columns.map(&:name)
183     api_column_map = Hash.new { |hash, key| hash[key] = [] }
184     methods.grep(/^api_accessible_\w+$/).each do |method_name|
185       next if method_name == :api_accessible_attributes
186       send(method_name).each_pair do |api_attr_name, col_name|
187         col_name = col_name.to_s
188         if all_columns.include?(col_name)
189           api_column_map[api_attr_name.to_s] |= [col_name]
190         end
191       end
192     end
193     api_column_map
194   end
195
196   def self.ignored_select_attributes
197     ["href", "kind", "etag"]
198   end
199
200   def self.columns_for_attributes(select_attributes)
201     if select_attributes.empty?
202       raise ArgumentError.new("Attribute selection list cannot be empty")
203     end
204     api_column_map = attributes_required_columns
205     invalid_attrs = []
206     select_attributes.each do |s|
207       next if ignored_select_attributes.include? s
208       if not s.is_a? String or not api_column_map.include? s
209         invalid_attrs << s
210       end
211     end
212     if not invalid_attrs.empty?
213       raise ArgumentError.new("Invalid attribute(s): #{invalid_attrs.inspect}")
214     end
215     # Given an array of attribute names to select, return an array of column
216     # names that must be fetched from the database to satisfy the request.
217     select_attributes.flat_map { |attr| api_column_map[attr] }.uniq
218   end
219
220   def self.default_orders
221     ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
222   end
223
224   def self.unique_columns
225     ["id", "uuid"]
226   end
227
228   def self.limit_index_columns_read
229     # This method returns a list of column names.
230     # If an index request reads that column from the database,
231     # APIs that return lists will only fetch objects until reaching
232     # max_index_database_read bytes of data from those columns.
233     []
234   end
235
236   # If current user can manage the object, return an array of uuids of
237   # users and groups that have permission to write the object. The
238   # first two elements are always [self.owner_uuid, current user's
239   # uuid].
240   #
241   # If current user can write but not manage the object, return
242   # [self.owner_uuid, current user's uuid].
243   #
244   # If current user cannot write this object, just return
245   # [self.owner_uuid].
246   def writable_by
247     return [owner_uuid] if not current_user
248     unless (owner_uuid == current_user.uuid or
249             current_user.is_admin or
250             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
251       if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
252           [uuid, owner_uuid]).any?
253         return [owner_uuid, current_user.uuid]
254       else
255         return [owner_uuid]
256       end
257     end
258     [owner_uuid, current_user.uuid] + permissions.collect do |p|
259       if ['can_write', 'can_manage'].index p.name
260         p.tail_uuid
261       end
262     end.compact.uniq
263   end
264
265   # Return a query with read permissions restricted to the union of the
266   # permissions of the members of users_list, i.e. if something is readable by
267   # any user in users_list, it will be readable in the query returned by this
268   # function.
269   def self.readable_by(*users_list)
270     # Get rid of troublesome nils
271     users_list.compact!
272
273     # Load optional keyword arguments, if they exist.
274     if users_list.last.is_a? Hash
275       kwargs = users_list.pop
276     else
277       kwargs = {}
278     end
279
280     # Collect the UUIDs of the authorized users.
281     sql_table = kwargs.fetch(:table_name, table_name)
282     include_trash = kwargs.fetch(:include_trash, false)
283     include_old_versions = kwargs.fetch(:include_old_versions, false)
284
285     sql_conds = nil
286     user_uuids = users_list.map { |u| u.uuid }
287
288     exclude_trashed_records = ""
289     if !include_trash and (sql_table == "groups" or sql_table == "collections") then
290       # Only include records that are not explicitly trashed
291       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
292     end
293
294     if users_list.select { |u| u.is_admin }.any?
295       # Admin skips most permission checks, but still want to filter on trashed items.
296       if !include_trash
297         if sql_table != "api_client_authorizations"
298           # Only include records where the owner is not trashed
299           #sql_conds = "#{sql_table}.owner_uuid NOT IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
300           #            "WHERE trashed = 1) #{exclude_trashed_records}"
301           sql_conds = "#{sql_table}.owner_uuid NOT IN (SELECT group_uuid FROM trashed_groups where trash_at < clock_timestamp()) #{exclude_trashed_records}"
302         end
303       end
304     else
305       trashed_check = ""
306       if !include_trash then
307         #trashed_check = "AND trashed = 0"
308         trashed_check = "AND target_uuid NOT IN (SELECT group_uuid FROM trashed_groups where trash_at < clock_timestamp())"
309       end
310
311       # Note: it is possible to combine the direct_check and
312       # owner_check into a single EXISTS() clause, however it turns
313       # out query optimizer doesn't like it and forces a sequential
314       # table scan.  Constructing the query with separate EXISTS()
315       # clauses enables it to use the index.
316       #
317       # see issue 13208 for details.
318
319       # Match a direct read permission link from the user to the record uuid
320       direct_check = "#{sql_table}.uuid IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
321                      "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check})"
322
323       # Match a read permission link from the user to the record's owner_uuid
324       owner_check = ""
325       if sql_table != "api_client_authorizations" and sql_table != "groups" then
326         owner_check = "OR #{sql_table}.owner_uuid IN (SELECT target_uuid FROM #{PERMISSION_VIEW} "+
327           "WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 #{trashed_check} AND target_owner_uuid IS NOT NULL) "
328       end
329
330       links_cond = ""
331       if sql_table == "links"
332         # Match any permission link that gives one of the authorized
333         # users some permission _or_ gives anyone else permission to
334         # view one of the authorized users.
335         links_cond = "OR (#{sql_table}.link_class IN (:permission_link_classes) AND "+
336                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
337       end
338
339       sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
340
341     end
342
343     if !include_old_versions && sql_table == "collections"
344       exclude_old_versions = "#{sql_table}.uuid = #{sql_table}.current_version_uuid"
345       if sql_conds.nil?
346         sql_conds = exclude_old_versions
347       else
348         sql_conds += " AND #{exclude_old_versions}"
349       end
350     end
351
352     self.where(sql_conds,
353                user_uuids: user_uuids,
354                permission_link_classes: ['permission', 'resources'])
355   end
356
357   def save_with_unique_name!
358     uuid_was = uuid
359     name_was = name
360     max_retries = 2
361     transaction do
362       conn = ActiveRecord::Base.connection
363       conn.exec_query 'SAVEPOINT save_with_unique_name'
364       begin
365         save!
366       rescue ActiveRecord::RecordNotUnique => rn
367         raise if max_retries == 0
368         max_retries -= 1
369
370         conn.exec_query 'ROLLBACK TO SAVEPOINT save_with_unique_name'
371
372         # Dig into the error to determine if it is specifically calling out a
373         # (owner_uuid, name) uniqueness violation.  In this specific case, and
374         # the client requested a unique name with ensure_unique_name==true,
375         # update the name field and try to save again.  Loop as necessary to
376         # discover a unique name.  It is necessary to handle name choosing at
377         # this level (as opposed to the client) to ensure that record creation
378         # never fails due to a race condition.
379         err = rn.cause
380         raise unless err.is_a?(PG::UniqueViolation)
381
382         # Unfortunately ActiveRecord doesn't abstract out any of the
383         # necessary information to figure out if this the error is actually
384         # the specific case where we want to apply the ensure_unique_name
385         # behavior, so the following code is specialized to Postgres.
386         detail = err.result.error_field(PG::Result::PG_DIAG_MESSAGE_DETAIL)
387         raise unless /^Key \(owner_uuid, name\)=\([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}, .*?\) already exists\./.match detail
388
389         new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
390         if new_name == name
391           # If the database is fast enough to do two attempts in the
392           # same millisecond, we need to wait to ensure we try a
393           # different timestamp on each attempt.
394           sleep 0.002
395           new_name = "#{name_was} (#{db_current_time.utc.iso8601(3)})"
396         end
397
398         self[:name] = new_name
399         if uuid_was.nil? && !uuid.nil?
400           self[:uuid] = nil
401           if self.is_a? Collection
402             # Reset so that is assigned to the new UUID
403             self[:current_version_uuid] = nil
404           end
405         end
406         conn.exec_query 'SAVEPOINT save_with_unique_name'
407         retry
408       ensure
409         conn.exec_query 'RELEASE SAVEPOINT save_with_unique_name'
410       end
411     end
412   end
413
414   def user_owner_uuid
415     if self.owner_uuid.nil?
416       return current_user.uuid
417     end
418     owner_class = ArvadosModel.resource_class_for_uuid(self.owner_uuid)
419     if owner_class == User
420       self.owner_uuid
421     else
422       owner_class.find_by_uuid(self.owner_uuid).user_owner_uuid
423     end
424   end
425
426   def logged_attributes
427     attributes.except(*Rails.configuration.AuditLogs.UnloggedAttributes.keys)
428   end
429
430   def self.full_text_searchable_columns
431     self.columns.select do |col|
432       [:string, :text, :jsonb].include?(col.type)
433     end.map(&:name)
434   end
435
436   def self.full_text_coalesce
437     full_text_searchable_columns.collect do |column|
438       is_jsonb = self.columns.select{|x|x.name == column}[0].type == :jsonb
439       cast = (is_jsonb || serialized_attributes[column]) ? '::text' : ''
440       "coalesce(#{column}#{cast},'')"
441     end
442   end
443
444   def self.full_text_trgm
445     "(#{full_text_coalesce.join(" || ' ' || ")})"
446   end
447
448   def self.full_text_tsvector
449     parts = full_text_searchable_columns.collect do |column|
450       is_jsonb = self.columns.select{|x|x.name == column}[0].type == :jsonb
451       cast = (is_jsonb || serialized_attributes[column]) ? '::text' : ''
452       "coalesce(#{column}#{cast},'')"
453     end
454     "to_tsvector('english', substr(#{parts.join(" || ' ' || ")}, 0, 8000))"
455   end
456
457   def self.apply_filters query, filters
458     ft = record_filters filters, self
459     if not ft[:cond_out].any?
460       return query
461     end
462     ft[:joins].each do |t|
463       query = query.joins(t)
464     end
465     query.where('(' + ft[:cond_out].join(') AND (') + ')',
466                           *ft[:param_out])
467   end
468
469   protected
470
471   def self.deep_sort_hash(x)
472     if x.is_a? Hash
473       x.sort.collect do |k, v|
474         [k, deep_sort_hash(v)]
475       end.to_h
476     elsif x.is_a? Array
477       x.collect { |v| deep_sort_hash(v) }
478     else
479       x
480     end
481   end
482
483   def ensure_ownership_path_leads_to_user
484     if new_record? or owner_uuid_changed?
485       uuid_in_path = {owner_uuid => true, uuid => true}
486       x = owner_uuid
487       while (owner_class = ArvadosModel::resource_class_for_uuid(x)) != User
488         begin
489           if x == uuid
490             # Test for cycles with the new version, not the DB contents
491             x = owner_uuid
492           elsif !owner_class.respond_to? :find_by_uuid
493             raise ActiveRecord::RecordNotFound.new
494           else
495             x = owner_class.find_by_uuid(x).owner_uuid
496           end
497         rescue ActiveRecord::RecordNotFound => e
498           errors.add :owner_uuid, "is not owned by any user: #{e}"
499           throw(:abort)
500         end
501         if uuid_in_path[x]
502           if x == owner_uuid
503             errors.add :owner_uuid, "would create an ownership cycle"
504           else
505             errors.add :owner_uuid, "has an ownership cycle"
506           end
507           throw(:abort)
508         end
509         uuid_in_path[x] = true
510       end
511     end
512     true
513   end
514
515   def set_default_owner
516     if new_record? and current_user and respond_to? :owner_uuid=
517       self.owner_uuid ||= current_user.uuid
518     end
519   end
520
521   def ensure_owner_uuid_is_permitted
522     raise PermissionDeniedError if !current_user
523
524     if self.owner_uuid.nil?
525       errors.add :owner_uuid, "cannot be nil"
526       raise PermissionDeniedError
527     end
528
529     rsc_class = ArvadosModel::resource_class_for_uuid owner_uuid
530     unless rsc_class == User or rsc_class == Group
531       errors.add :owner_uuid, "must be set to User or Group"
532       raise PermissionDeniedError
533     end
534
535     if new_record? || owner_uuid_changed?
536       # Permission on owner_uuid_was is needed to move an existing
537       # object away from its previous owner (which implies permission
538       # to modify this object itself, so we don't need to check that
539       # separately). Permission on the new owner_uuid is also needed.
540       [['old', owner_uuid_was],
541        ['new', owner_uuid]
542       ].each do |which, check_uuid|
543         if check_uuid.nil?
544           # old_owner_uuid is nil? New record, no need to check.
545         elsif !current_user.can?(write: check_uuid)
546           logger.warn "User #{current_user.uuid} tried to set ownership of #{self.class.to_s} #{self.uuid} but does not have permission to write #{which} owner_uuid #{check_uuid}"
547           errors.add :owner_uuid, "cannot be set or changed without write permission on #{which} owner"
548           raise PermissionDeniedError
549         end
550       end
551     else
552       # If the object already existed and we're not changing
553       # owner_uuid, we only need write permission on the object
554       # itself.
555       if !current_user.can?(write: self.uuid)
556         logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} without write permission"
557         errors.add :uuid, "is not writable"
558         raise PermissionDeniedError
559       end
560     end
561
562     true
563   end
564
565   def ensure_permission_to_save
566     unless (new_record? ? permission_to_create : permission_to_update)
567       raise PermissionDeniedError
568     end
569   end
570
571   def permission_to_create
572     current_user.andand.is_active
573   end
574
575   def permission_to_update
576     if !current_user
577       logger.warn "Anonymous user tried to update #{self.class.to_s} #{self.uuid_was}"
578       return false
579     end
580     if !current_user.is_active
581       logger.warn "Inactive user #{current_user.uuid} tried to update #{self.class.to_s} #{self.uuid_was}"
582       return false
583     end
584     return true if current_user.is_admin
585     if self.uuid_changed?
586       logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
587       return false
588     end
589     return true
590   end
591
592   def ensure_permission_to_destroy
593     raise PermissionDeniedError unless permission_to_destroy
594   end
595
596   def permission_to_destroy
597     permission_to_update
598   end
599
600   def maybe_update_modified_by_fields
601     update_modified_by_fields if self.changed? or self.new_record?
602     true
603   end
604
605   def update_modified_by_fields
606     current_time = db_current_time
607     self.created_at ||= created_at_was || current_time
608     self.updated_at = current_time
609     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
610     if !anonymous_updater
611       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
612     end
613     if !timeless_updater
614       self.modified_at = current_time
615     end
616     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
617     true
618   end
619
620   def self.has_nonstring_keys? x
621     if x.is_a? Hash
622       x.each do |k,v|
623         return true if !(k.is_a?(String) || k.is_a?(Symbol)) || has_nonstring_keys?(v)
624       end
625     elsif x.is_a? Array
626       x.each do |v|
627         return true if has_nonstring_keys?(v)
628       end
629     end
630     false
631   end
632
633   def self.where_serialized(colname, value, md5: false)
634     colsql = colname.to_s
635     if md5
636       colsql = "md5(#{colsql})"
637     end
638     if value.empty?
639       # rails4 stores as null, rails3 stored as serialized [] or {}
640       sql = "#{colsql} is null or #{colsql} IN (?)"
641       sorted = value
642     else
643       sql = "#{colsql} IN (?)"
644       sorted = deep_sort_hash(value)
645     end
646     params = [sorted.to_yaml, SafeJSON.dump(sorted)]
647     if md5
648       params = params.map { |x| Digest::MD5.hexdigest(x) }
649     end
650     where(sql, params)
651   end
652
653   Serializer = {
654     Hash => HashSerializer,
655     Array => ArraySerializer,
656   }
657
658   def self.serialize(colname, type)
659     coder = Serializer[type]
660     @serialized_attributes ||= {}
661     @serialized_attributes[colname.to_s] = coder
662     super(colname, coder)
663   end
664
665   def self.serialized_attributes
666     @serialized_attributes ||= {}
667   end
668
669   def serialized_attributes
670     self.class.serialized_attributes
671   end
672
673   def foreign_key_attributes
674     attributes.keys.select { |a| a.match(/_uuid$/) }
675   end
676
677   def skip_uuid_read_permission_check
678     %w(modified_by_client_uuid)
679   end
680
681   def skip_uuid_existence_check
682     []
683   end
684
685   def normalize_collection_uuids
686     foreign_key_attributes.each do |attr|
687       attr_value = send attr
688       if attr_value.is_a? String and
689           attr_value.match(/^[0-9a-f]{32,}(\+[@\w]+)*$/)
690         begin
691           send "#{attr}=", Collection.normalize_uuid(attr_value)
692         rescue
693           # TODO: abort instead of silently accepting unnormalizable value?
694         end
695       end
696     end
697   end
698
699   @@prefixes_hash = nil
700   def self.uuid_prefixes
701     unless @@prefixes_hash
702       @@prefixes_hash = {}
703       Rails.application.eager_load!
704       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
705         if k.respond_to?(:uuid_prefix)
706           @@prefixes_hash[k.uuid_prefix] = k
707         end
708       end
709     end
710     @@prefixes_hash
711   end
712
713   def self.uuid_like_pattern
714     "#{Rails.configuration.ClusterID}-#{uuid_prefix}-_______________"
715   end
716
717   def self.uuid_regex
718     %r/[a-z0-9]{5}-#{uuid_prefix}-[a-z0-9]{15}/
719   end
720
721   def ensure_valid_uuids
722     specials = [system_user_uuid]
723
724     foreign_key_attributes.each do |attr|
725       if new_record? or send (attr + "_changed?")
726         next if skip_uuid_existence_check.include? attr
727         attr_value = send attr
728         next if specials.include? attr_value
729         if attr_value
730           if (r = ArvadosModel::resource_class_for_uuid attr_value)
731             unless skip_uuid_read_permission_check.include? attr
732               r = r.readable_by(current_user)
733             end
734             if r.where(uuid: attr_value).count == 0
735               errors.add(attr, "'#{attr_value}' not found")
736             end
737           end
738         end
739       end
740     end
741   end
742
743   def ensure_filesystem_compatible_name
744     if name == "." || name == ".."
745       errors.add(:name, "cannot be '.' or '..'")
746     elsif Rails.configuration.Collections.ForwardSlashNameSubstitution == "" && !name.nil? && name.index('/')
747       errors.add(:name, "cannot contain a '/' character")
748     end
749   end
750
751   class Email
752     def self.kind
753       "email"
754     end
755
756     def kind
757       self.class.kind
758     end
759
760     def self.readable_by (*u)
761       self
762     end
763
764     def self.where (u)
765       [{:uuid => u[:uuid]}]
766     end
767   end
768
769   def self.resource_class_for_uuid(uuid)
770     if uuid.is_a? ArvadosModel
771       return uuid.class
772     end
773     unless uuid.is_a? String
774       return nil
775     end
776
777     uuid.match HasUuid::UUID_REGEX do |re|
778       return uuid_prefixes[re[1]] if uuid_prefixes[re[1]]
779     end
780
781     if uuid.match(/.+@.+/)
782       return Email
783     end
784
785     nil
786   end
787
788   # ArvadosModel.find_by_uuid needs extra magic to allow it to return
789   # an object in any class.
790   def self.find_by_uuid uuid
791     if self == ArvadosModel
792       # If called directly as ArvadosModel.find_by_uuid rather than via subclass,
793       # delegate to the appropriate subclass based on the given uuid.
794       self.resource_class_for_uuid(uuid).find_by_uuid(uuid)
795     else
796       super
797     end
798   end
799
800   def is_audit_logging_enabled?
801     return !(Rails.configuration.AuditLogs.MaxAge.to_i == 0 &&
802              Rails.configuration.AuditLogs.MaxDeleteBatch.to_i > 0)
803   end
804
805   def log_start_state
806     if is_audit_logging_enabled?
807       @old_attributes = Marshal.load(Marshal.dump(attributes))
808       @old_logged_attributes = Marshal.load(Marshal.dump(logged_attributes))
809     end
810   end
811
812   def log_change(event_type)
813     if is_audit_logging_enabled?
814       log = Log.new(event_type: event_type).fill_object(self)
815       yield log
816       log.save!
817       log_start_state
818     end
819   end
820
821   def log_create
822     if is_audit_logging_enabled?
823       log_change('create') do |log|
824         log.fill_properties('old', nil, nil)
825         log.update_to self
826       end
827     end
828   end
829
830   def log_update
831     if is_audit_logging_enabled?
832       log_change('update') do |log|
833         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
834         log.update_to self
835       end
836     end
837   end
838
839   def log_destroy
840     if is_audit_logging_enabled?
841       log_change('delete') do |log|
842         log.fill_properties('old', etag(@old_attributes), @old_logged_attributes)
843         log.update_to nil
844       end
845     end
846   end
847 end