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