tina4ruby 3.13.37 → 3.13.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/tina4/session.rb CHANGED
@@ -21,7 +21,16 @@ module Tina4
21
21
  if !options.key?(:cookie_name) && env_name && !env_name.empty?
22
22
  @options[:cookie_name] = env_name
23
23
  end
24
- @options[:secret] ||= ENV["TINA4_SECRET"] || "tina4-default-secret"
24
+ # No guessable built-in secret. The session never signs with this value
25
+ # (IDs are SecureRandom.hex(32)), so we resolve it from TINA4_SECRET only
26
+ # — nil when unset. This honours the framework's blank-secret discipline
27
+ # (Auth.ensure_dev_secret never uses a guessable default); Python/Node
28
+ # sessions carry no secret field at all.
29
+ @options[:secret] ||= ENV["TINA4_SECRET"]
30
+ # Backend-failure policy strict flag (parity with Python's
31
+ # TINA4_SESSION_STRICT). When truthy, read/write/destroy/gc failures
32
+ # RE-RAISE instead of logging + degrading.
33
+ @strict = Tina4::Env.is_truthy(ENV["TINA4_SESSION_STRICT"])
25
34
  @handler = create_handler
26
35
  @id = extract_session_id(env) || SecureRandom.hex(32)
27
36
  @data = load_session
@@ -51,14 +60,23 @@ module Tina4
51
60
  @data.dup
52
61
  end
53
62
 
63
+ # Persist the session if dirty. On a backend write failure the error is
64
+ # logged and false is returned — the @modified (dirty) flag is RETAINED so
65
+ # a later save can retry. Returns true on a successful (or no-op) write.
54
66
  def save
55
- return unless @modified
56
- @handler.write(@id, @data)
57
- @modified = false
67
+ return true unless @modified
68
+ if safe_write(@id, @data)
69
+ @modified = false
70
+ true
71
+ else
72
+ false # dirty flag retained for retry
73
+ end
58
74
  end
59
75
 
76
+ # Destroy the current session. Should be called right after login or any
77
+ # privilege change to defend against session fixation (see #regenerate).
60
78
  def destroy
61
- @handler.destroy(@id)
79
+ safe_destroy(@id)
62
80
  @data = {}
63
81
  end
64
82
 
@@ -104,12 +122,17 @@ module Tina4
104
122
  result.nil? ? default : result
105
123
  end
106
124
 
107
- # Regenerate the session ID while preserving data — returns new ID
125
+ # Regenerate the session ID while preserving data — returns the new ID.
126
+ # Call this right after login or any privilege change to defend against
127
+ # session fixation (a pre-auth session ID must not survive into the
128
+ # authenticated session). Destroys the old backend record (best-effort)
129
+ # and persists under the new ID.
108
130
  def regenerate
109
131
  old_id = @id
110
132
  @id = SecureRandom.hex(32)
111
- @handler.destroy(old_id)
133
+ safe_destroy(old_id)
112
134
  @modified = true
135
+ save
113
136
  @id
114
137
  end
115
138
 
@@ -133,24 +156,27 @@ module Tina4
133
156
  end
134
157
 
135
158
  # Reads raw session data for a given session ID from backend storage.
136
- # Returns the data hash or nil.
159
+ # Returns the data hash, or {} on a backend failure (logged + degraded).
137
160
  def read(session_id)
138
- @handler.read(session_id)
161
+ safe_read(session_id)
139
162
  end
140
163
 
141
164
  # Writes raw session data for a given session ID to backend storage.
165
+ # Returns true on success, false on a backend failure (logged + degraded).
142
166
  def write(session_id, data, ttl = nil)
143
- if ttl
144
- @handler.write(session_id, data, ttl)
145
- else
146
- @handler.write(session_id, data)
147
- end
167
+ safe_write(session_id, data, ttl)
148
168
  end
149
169
 
150
- # Garbage collection: remove expired sessions from the handler
170
+ # Garbage collection: remove expired sessions from the handler.
171
+ # A backend failure is logged and swallowed (never crashes the request).
151
172
  def gc(max_lifetime = nil)
173
+ return unless @handler.respond_to?(:gc)
152
174
  max_lifetime ||= @options[:max_age]
153
- @handler.gc(max_lifetime) if @handler.respond_to?(:gc)
175
+ @handler.gc(max_lifetime)
176
+ rescue StandardError => e
177
+ log_backend_error("gc", e)
178
+ raise if @strict
179
+ nil
154
180
  end
155
181
 
156
182
  def cookie_header(cookie_name = nil)
@@ -181,8 +207,59 @@ module Tina4
181
207
  end
182
208
 
183
209
  def load_session
184
- existing = @handler.read(@id)
210
+ safe_read(@id)
211
+ end
212
+
213
+ # ── Backend-failure policy (parity with Python's Session boundary) ──
214
+ #
215
+ # Centralised here, NOT in each handler, so every backend (file, redis,
216
+ # valkey, mongo, database) shares one policy. The rule:
217
+ # read failure → log + return {} (empty session, never a 500)
218
+ # write failure → log + return false (caller retains dirty for retry)
219
+ # destroy failure → log + swallow (return false)
220
+ # gc failure → log + swallow (see #gc)
221
+ # A genuinely-empty but HEALTHY backend (handler returns nil/{} WITHOUT
222
+ # raising) is NOT a failure and logs nothing. TINA4_SESSION_STRICT=true
223
+ # re-raises instead of degrading.
224
+
225
+ def safe_read(session_id)
226
+ existing = @handler.read(session_id)
185
227
  existing || {}
228
+ rescue StandardError => e
229
+ log_backend_error("read", e)
230
+ raise if @strict
231
+ {}
232
+ end
233
+
234
+ def safe_write(session_id, data, ttl = nil)
235
+ if ttl
236
+ @handler.write(session_id, data, ttl)
237
+ else
238
+ @handler.write(session_id, data)
239
+ end
240
+ true
241
+ rescue StandardError => e
242
+ log_backend_error("write", e)
243
+ raise if @strict
244
+ false
245
+ end
246
+
247
+ def safe_destroy(session_id)
248
+ @handler.destroy(session_id)
249
+ true
250
+ rescue StandardError => e
251
+ log_backend_error("destroy", e)
252
+ raise if @strict
253
+ false
254
+ end
255
+
256
+ # Single source of the backend-failure log line. Names the operation and
257
+ # the concrete handler class so ops can see WHICH backend failed.
258
+ def log_backend_error(operation, error)
259
+ handler_class = @handler.class.name
260
+ Tina4::Log.error("Session #{operation} failed (#{handler_class}): #{error.message}")
261
+ rescue StandardError
262
+ warn("Session #{operation} failed: #{error.message}")
186
263
  end
187
264
 
188
265
  def create_handler
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.37"
4
+ VERSION = "3.13.39"
5
5
  end