tina4ruby 3.13.159 → 3.13.160
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/tina4/middleware.rb +29 -6
- data/lib/tina4/rack_app.rb +36 -8
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04d0f0c9549b131ac6cc66c25af963c658b14a34c5f1690c17c46b2df16b0ab5
|
|
4
|
+
data.tar.gz: 8ae3b7fb96142e89e7db778c790f45b53b763e1d2531870bbda84a5cf5422c83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f50bfc2ae1a344b6398760b720caf2fc50cf9b18858ce64c2cd2ea1be4422537952a74028c34aea84244bd84e42e05f387f3c1e7830be48b7eaca7c0d8d58f7
|
|
7
|
+
data.tar.gz: 3a8666bf0297807ddaee244b20d64e187dd76af5d8004555f10b19c79e6015d5620e42f86fab423b16ba6ac85d7d0347028491659798967cc36626e756e80547
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.160 - 2026-11-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- The secure-by-default auth gate answered a tokenless request to a protected route with
|
|
13
|
+
a raw `401 {"error": "Unauthorized"}` JSON tuple whatever the client's `Accept`, and a
|
|
14
|
+
role/permission-guarded (RBAC) route answered a bare `403` the same way — so a browser
|
|
15
|
+
that posted a form after its session expired saw raw JSON instead of the error page. Both
|
|
16
|
+
now route through a shared negotiated renderer (`RackApp.negotiated_error_tuple`), the same
|
|
17
|
+
negotiation every other framework error already used (ERR-DEC-02): a browser gets
|
|
18
|
+
`errors/401.twig` / `errors/403.twig`, an API client the
|
|
19
|
+
`{error, code, message, status, request_id}` envelope (#342).
|
|
20
|
+
- `Tina4::Middleware.middleware_500` answered a raw `500 {"error": "Internal Server Error",
|
|
21
|
+
"status": 500}` body regardless of `Accept` and with no request id. It now negotiates the
|
|
22
|
+
same way — `errors/500.twig` for a browser, the JSON envelope with the request id for an
|
|
23
|
+
API client — while keeping the exception out of the body (CWE-209) (#342).
|
|
24
|
+
- `401` gained its `UNAUTHORIZED` entry in the error-code-name map, so the negotiated 401
|
|
25
|
+
JSON envelope carries a real code rather than `HTTP_401`.
|
|
26
|
+
|
|
9
27
|
## 3.13.159 - 2026-11-06
|
|
10
28
|
|
|
11
29
|
### Fixed
|
data/lib/tina4/middleware.rb
CHANGED
|
@@ -114,7 +114,7 @@ module Tina4
|
|
|
114
114
|
begin
|
|
115
115
|
result = entry[:handler].call(request, response)
|
|
116
116
|
rescue StandardError, ScriptError => error
|
|
117
|
-
middleware_500(response, "before handler", error)
|
|
117
|
+
middleware_500(response, "before handler", error, request)
|
|
118
118
|
return false
|
|
119
119
|
end
|
|
120
120
|
return false if result == false
|
|
@@ -126,7 +126,7 @@ module Tina4
|
|
|
126
126
|
begin
|
|
127
127
|
result = klass.send(method_name, request, response)
|
|
128
128
|
rescue StandardError, ScriptError => error
|
|
129
|
-
middleware_500(response, "#{class_label(klass)}.#{method_name}", error)
|
|
129
|
+
middleware_500(response, "#{class_label(klass)}.#{method_name}", error, request)
|
|
130
130
|
return false
|
|
131
131
|
end
|
|
132
132
|
|
|
@@ -174,7 +174,7 @@ module Tina4
|
|
|
174
174
|
begin
|
|
175
175
|
entry[:handler].call(request, response)
|
|
176
176
|
rescue StandardError, ScriptError => error
|
|
177
|
-
middleware_500(response, "after handler", error)
|
|
177
|
+
middleware_500(response, "after handler", error, request)
|
|
178
178
|
end
|
|
179
179
|
end
|
|
180
180
|
|
|
@@ -184,7 +184,7 @@ module Tina4
|
|
|
184
184
|
begin
|
|
185
185
|
result = klass.send(method_name, request, response)
|
|
186
186
|
rescue StandardError, ScriptError => error
|
|
187
|
-
middleware_500(response, "#{class_label(klass)}.#{method_name}", error)
|
|
187
|
+
middleware_500(response, "#{class_label(klass)}.#{method_name}", error, request)
|
|
188
188
|
next
|
|
189
189
|
end
|
|
190
190
|
if result.is_a?(Tina4::Response)
|
|
@@ -203,7 +203,7 @@ module Tina4
|
|
|
203
203
|
# (NEVER silent) then sets the response to the canonical error shape —
|
|
204
204
|
# byte-identical to the Python master ({"error":"Internal Server Error",
|
|
205
205
|
# "status":500} + status 500). Returns the response for chaining.
|
|
206
|
-
def middleware_500(response, label, error)
|
|
206
|
+
def middleware_500(response, label, error, request = nil)
|
|
207
207
|
begin
|
|
208
208
|
Tina4::Log.error(
|
|
209
209
|
"Middleware #{label} raised #{error.class.name}: #{error.message}"
|
|
@@ -216,7 +216,30 @@ module Tina4
|
|
|
216
216
|
# never let logging break the worker
|
|
217
217
|
end
|
|
218
218
|
end
|
|
219
|
-
|
|
219
|
+
# ERR-DEC-02 (#342): answer through the SAME negotiation every other
|
|
220
|
+
# framework error uses — errors/500.twig for a browser, the JSON envelope
|
|
221
|
+
# {error, code, message, status, request_id} for an API client. The real
|
|
222
|
+
# exception stays in the log, never in the body (CWE-209). It used to
|
|
223
|
+
# return an unconditional {"error":"Internal Server Error","status":500}.
|
|
224
|
+
# `request` is optional only for backward compatibility; the dispatcher
|
|
225
|
+
# always passes it, and without it the JSON envelope is used.
|
|
226
|
+
request_id = Tina4::Log.get_request_id || ""
|
|
227
|
+
json_body = { error: true, code: "INTERNAL_SERVER_ERROR", message: "Internal Server Error", status: 500, request_id: request_id }
|
|
228
|
+
accept = request.respond_to?(:headers) ? (request.headers["accept"] || "") : ""
|
|
229
|
+
return response.json(json_body, 500) if Tina4::Template.wants_json?(accept)
|
|
230
|
+
|
|
231
|
+
path = request.respond_to?(:path) ? request.path.to_s : ""
|
|
232
|
+
html = begin
|
|
233
|
+
Tina4::Template.render_error(500, { "path" => CGI.escapeHTML(path), "request_id" => request_id, "error_message" => "" })
|
|
234
|
+
rescue StandardError
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
if html && response.respond_to?(:html)
|
|
238
|
+
response.html(html, 500)
|
|
239
|
+
else
|
|
240
|
+
response.json(json_body, 500)
|
|
241
|
+
end
|
|
242
|
+
response
|
|
220
243
|
end
|
|
221
244
|
|
|
222
245
|
# The `false` row of the return-value table, on its own.
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -422,7 +422,9 @@ module Tina4
|
|
|
422
422
|
# (error: true, code, message, status) already shared by app-level
|
|
423
423
|
# response.error() calls, plus request_id for correlation (feature 43,
|
|
424
424
|
# ERR-404-REQUESTID) - the SAME shape Python/PHP/Node build.
|
|
425
|
-
ERROR_CODE_NAMES = {
|
|
425
|
+
ERROR_CODE_NAMES = {
|
|
426
|
+
401 => "UNAUTHORIZED", 403 => "FORBIDDEN", 404 => "NOT_FOUND", 500 => "INTERNAL_SERVER_ERROR"
|
|
427
|
+
}.freeze
|
|
426
428
|
|
|
427
429
|
def error_json_body(status, message, request_id)
|
|
428
430
|
Tina4::Response.error_response(
|
|
@@ -1236,10 +1238,34 @@ module Tina4
|
|
|
1236
1238
|
# in-process TestClient call it, so the test surface enforces the identical
|
|
1237
1239
|
# gate as production (Python #PY2 parity). Instantiating a RackApp just to
|
|
1238
1240
|
# reach the check would run full boot/route-discovery — this needs none of it.
|
|
1241
|
+
# Build a negotiated error Rack tuple (ERR-DEC-02, #342): a browser gets
|
|
1242
|
+
# errors/{code}.twig, an API client the JSON envelope, both carrying
|
|
1243
|
+
# request_id. A CLASS method so the class-method auth / RBAC gates
|
|
1244
|
+
# (enforce_route_auth, rbac_forbidden) answer like every other framework
|
|
1245
|
+
# error; the instance handle_403/handle_404/handle_500 paths keep their own
|
|
1246
|
+
# copies (they carry extra template-serving / debug-overlay logic).
|
|
1247
|
+
def self.negotiated_error_tuple(code, message, path = "", accept = "")
|
|
1248
|
+
request_id = Tina4::Log.get_request_id || "-"
|
|
1249
|
+
if Tina4::Template.wants_json?(accept)
|
|
1250
|
+
body = Tina4::Response.error_response(ERROR_CODE_NAMES[code] || "HTTP_#{code}", message, code)
|
|
1251
|
+
.merge(request_id: request_id)
|
|
1252
|
+
return [code, { "content-type" => "application/json" }, [JSON.generate(body)]]
|
|
1253
|
+
end
|
|
1254
|
+
html = begin
|
|
1255
|
+
Tina4::Template.render_error(code, { "path" => CGI.escapeHTML(path.to_s), "request_id" => request_id, "error_message" => "" })
|
|
1256
|
+
rescue StandardError
|
|
1257
|
+
"#{code} #{message}"
|
|
1258
|
+
end
|
|
1259
|
+
[code, { "content-type" => "text/html" }, [html]]
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1239
1262
|
def self.enforce_route_auth(env, route)
|
|
1240
1263
|
return nil unless route.auth_required
|
|
1241
1264
|
|
|
1242
|
-
|
|
1265
|
+
# ERR-DEC-02 (#342): a browser gets errors/401.twig, an API client the JSON
|
|
1266
|
+
# envelope — the same negotiation every other framework error uses. Was an
|
|
1267
|
+
# unconditional JSON tuple.
|
|
1268
|
+
unauthorized = negotiated_error_tuple(401, "Unauthorized", env["PATH_INFO"] || "", env["HTTP_ACCEPT"] || "")
|
|
1243
1269
|
|
|
1244
1270
|
# Token slots in priority order: Authorization Bearer header, then the
|
|
1245
1271
|
# body formToken (frond.js puts the auth token it received as a
|
|
@@ -1283,7 +1309,7 @@ module Tina4
|
|
|
1283
1309
|
# ── RBAC guards (Feature 138): authorization AFTER authentication ──
|
|
1284
1310
|
# Auth has passed (401 ruled out above). If the route carries role/
|
|
1285
1311
|
# permission guards, the verified payload must satisfy them, else 403.
|
|
1286
|
-
deny = rbac_forbidden(route, env["tina4.auth_payload"])
|
|
1312
|
+
deny = rbac_forbidden(route, env["tina4.auth_payload"], env)
|
|
1287
1313
|
return deny if deny
|
|
1288
1314
|
|
|
1289
1315
|
nil
|
|
@@ -1317,7 +1343,7 @@ module Tina4
|
|
|
1317
1343
|
# verified payload, else nil. AND across guard groups, OR within a group.
|
|
1318
1344
|
# Roles read the `roles` claim (legacy singular `role` coerced); permissions
|
|
1319
1345
|
# read `permissions` with granted-side wildcards. Feature 138 / ADR-0058.
|
|
1320
|
-
def self.rbac_forbidden(route, payload)
|
|
1346
|
+
def self.rbac_forbidden(route, payload, env = nil)
|
|
1321
1347
|
required_roles = route.respond_to?(:roles) ? Array(route.roles) : []
|
|
1322
1348
|
required_perms = route.respond_to?(:perms) ? Array(route.perms) : []
|
|
1323
1349
|
return nil if required_roles.empty? && required_perms.empty?
|
|
@@ -1325,17 +1351,19 @@ module Tina4
|
|
|
1325
1351
|
subject = payload.is_a?(Hash) ? payload : {}
|
|
1326
1352
|
roles = _rbac_claim_list(subject, "roles", "role")
|
|
1327
1353
|
required_roles.each do |group|
|
|
1328
|
-
return _rbac_forbidden_tuple unless group.any? { |r| roles.include?(r) }
|
|
1354
|
+
return _rbac_forbidden_tuple(env) unless group.any? { |r| roles.include?(r) }
|
|
1329
1355
|
end
|
|
1330
1356
|
perms = _rbac_claim_list(subject, "permissions", nil)
|
|
1331
1357
|
required_perms.each do |group|
|
|
1332
|
-
return _rbac_forbidden_tuple unless group.any? { |req| _rbac_perm_granted(perms, req) }
|
|
1358
|
+
return _rbac_forbidden_tuple(env) unless group.any? { |req| _rbac_perm_granted(perms, req) }
|
|
1333
1359
|
end
|
|
1334
1360
|
nil
|
|
1335
1361
|
end
|
|
1336
1362
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1363
|
+
# ERR-DEC-02 negotiation (#342): HTML page for a browser, JSON envelope for
|
|
1364
|
+
# an API client, both with request_id — instead of the old unconditional JSON.
|
|
1365
|
+
def self._rbac_forbidden_tuple(env = nil)
|
|
1366
|
+
negotiated_error_tuple(403, "Forbidden", env && env["PATH_INFO"] || "", env && env["HTTP_ACCEPT"] || "")
|
|
1339
1367
|
end
|
|
1340
1368
|
|
|
1341
1369
|
# Read a claim as a list of strings; coerce a legacy singular string.
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.160
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|