Merge branch '18004-cached-token-race-condition' into main. Closes #18004
[arvados.git] / services / api / lib / trashable.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 module Trashable
6   def self.included(base)
7     base.before_validation :set_validation_timestamp
8     base.before_validation :ensure_trash_at_not_in_past
9     base.before_validation :sync_trash_state
10     base.before_validation :default_trash_interval
11     base.validate :validate_trash_and_delete_timing
12   end
13
14   # Use a single timestamp for all validations, even though each
15   # validation runs at a different time.
16   def set_validation_timestamp
17     @validation_timestamp = db_current_time
18   end
19
20   # If trash_at is being changed to a time in the past, change it to
21   # now. This allows clients to say "expires {client-current-time}"
22   # without failing due to clock skew, while avoiding odd log entries
23   # like "expiry date changed to {1 year ago}".
24   def ensure_trash_at_not_in_past
25     if trash_at_changed? && trash_at
26       self.trash_at = [@validation_timestamp, trash_at].max
27     end
28   end
29
30   # Caller can move into/out of trash by setting/clearing is_trashed
31   # -- however, if the caller also changes trash_at, then any changes
32   # to is_trashed are ignored.
33   def sync_trash_state
34     if is_trashed_changed? && !trash_at_changed?
35       if is_trashed
36         self.trash_at = @validation_timestamp
37       else
38         self.trash_at = nil
39         self.delete_at = nil
40       end
41     end
42     self.is_trashed = trash_at && trash_at <= @validation_timestamp || false
43     true
44   end
45
46   def default_trash_interval
47     if trash_at_changed? && !delete_at_changed?
48       # If trash_at is updated without touching delete_at,
49       # automatically update delete_at to a sensible value.
50       if trash_at.nil?
51         self.delete_at = nil
52       else
53         self.delete_at = trash_at + Rails.configuration.Collections.DefaultTrashLifetime.seconds
54       end
55     elsif !trash_at || !delete_at || trash_at > delete_at
56       # Not trash, or bogus arguments? Just validate in
57       # validate_trash_and_delete_timing.
58     elsif delete_at_changed? && delete_at >= trash_at
59       # Fix delete_at if needed, so it's not earlier than the expiry
60       # time on any permission tokens that might have been given out.
61
62       # In any case there are no signatures expiring after now+TTL.
63       # Also, if the existing trash_at time has already passed, we
64       # know we haven't given out any signatures since then.
65       earliest_delete = [
66         @validation_timestamp,
67         trash_at_was,
68       ].compact.min + Rails.configuration.Collections.BlobSigningTTL
69
70       # The previous value of delete_at is also an upper bound on the
71       # longest-lived permission token. For example, if TTL=14,
72       # trash_at_was=now-7, delete_at_was=now+7, then it is safe to
73       # set trash_at=now+6, delete_at=now+8.
74       earliest_delete = [earliest_delete, delete_at_was].compact.min
75
76       # If delete_at is too soon, use the earliest possible time.
77       if delete_at < earliest_delete
78         self.delete_at = earliest_delete
79       end
80     end
81   end
82
83   def validate_trash_and_delete_timing
84     if trash_at.nil? != delete_at.nil?
85       errors.add :delete_at, "must be set if trash_at is set, and must be nil otherwise"
86     elsif delete_at && delete_at < trash_at
87       errors.add :delete_at, "must not be earlier than trash_at"
88     end
89     true
90   end
91 end
92
93 module TrashableController
94   def destroy
95     if !@object.is_trashed
96       @object.update_attributes!(trash_at: db_current_time)
97     end
98     earliest_delete = (@object.trash_at +
99                        Rails.configuration.Collections.BlobSigningTTL)
100     if @object.delete_at > earliest_delete
101       @object.update_attributes!(delete_at: earliest_delete)
102     end
103     show
104   end
105
106   def trash
107     if !@object.is_trashed
108       @object.update_attributes!(trash_at: db_current_time)
109     end
110     show
111   end
112
113   def untrash
114     if @object.is_trashed
115       @object.trash_at = nil
116
117       if params[:ensure_unique_name]
118         @object.save_with_unique_name!
119       else
120         @object.save!
121       end
122     else
123       raise ArvadosModel::InvalidStateTransitionError.new("Item is not trashed, cannot untrash")
124     end
125     show
126   end
127
128 end