tina4ruby 3.13.105 → 3.13.107
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 +68 -0
- data/lib/tina4/rack_app.rb +57 -0
- data/lib/tina4/router.rb +30 -0
- 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: ab8e75446e8ed41f28be0078b1f16de1e1c0be39ff83cefea98f2a5cc9a78cc2
|
|
4
|
+
data.tar.gz: b8d786867f6a37c87bbc0e242a615c7df8962ae8fd188d8bb8c381061ecba319
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7badcea95d972dfbc0171ed400722626d7d6565ff1b9328b1183dcedebe591c2c4fb2510fa67f8851614c4cd866c5b4db021351fdeea1bd016ce639ea1cb185f
|
|
7
|
+
data.tar.gz: b22a17a9907c81b37f1c22cb2d40e63c77eb62802f9db33ae749a747aa3d5eac5427523b2d5cec0a7ebda2b5d3c62ba88219fd906f0fab62da748a076f4b01e2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ 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.107
|
|
10
|
+
|
|
11
|
+
Feature: RBAC role and permission guards (parity across all four frameworks).
|
|
12
|
+
`role(...)` / `can(...)` route guards read the cryptographically-verified JWT
|
|
13
|
+
`roles` / `permissions` claims — OR within a guard, AND by stacking guards, with
|
|
14
|
+
granted-side wildcards (`posts.*`, `*`) and a legacy singular `role` claim coerced
|
|
15
|
+
to a list. A guarded route implies auth: no or invalid token → 401, authenticated
|
|
16
|
+
but unauthorised → 403; the handler never runs on a miss. Feature 138 / ADR-0058.
|
|
17
|
+
|
|
9
18
|
## 3.13.105
|
|
10
19
|
|
|
11
20
|
Bug release. Route inspection stops touching the app; Firebird's migration
|
|
@@ -26,6 +35,65 @@ filename that broke Windows checkouts.
|
|
|
26
35
|
Firebird driver returns for the `migration_name` column.
|
|
27
36
|
- Uses the atomic sequence table pattern already in place for other engines.
|
|
28
37
|
|
|
38
|
+
### Queue and ORM audit bugs closed
|
|
39
|
+
|
|
40
|
+
- **`Model.clear_cache()` cascades to `db.cache_clear()`.** Under both
|
|
41
|
+
`TINA4_AUTO_CACHING=true` and `TINA4_DB_CACHE=true`, a manual `clear_cache()`
|
|
42
|
+
used to leave the DB-layer cache holding stale rows (PY-06-22).
|
|
43
|
+
- **`Queue.retry()` revives every dead letter.** The no-arg form used a
|
|
44
|
+
generator inside `any(...)` and short-circuited after the first success.
|
|
45
|
+
Now materialised so all N dead letters revive (PY-12-04).
|
|
46
|
+
- **`Job.retry()` unlinks the dead-letter file.** Iterating `dead_letters()`
|
|
47
|
+
and calling `.retry()` on each used to leave the failed directory carrying
|
|
48
|
+
every revived job, so the next `dead_letters()` call re-reported them
|
|
49
|
+
(PY-12-05).
|
|
50
|
+
- **MongoDB `retry_job(id)` searches the dead-letter namespace by data.id,
|
|
51
|
+
deletes the DL doc, and upserts the original back to pending.** The old
|
|
52
|
+
filter `{_id, self._topic, "failed"}` could never match a dead letter.
|
|
53
|
+
- **MongoDB `purge(status)` returns `deleted_count`, honours every status, and
|
|
54
|
+
routes dead/failed/dead_letter aliases to the `.dead_letter` namespace.**
|
|
55
|
+
Previously returned None and only handled `pending` — via `clear()`, which
|
|
56
|
+
nuked every doc under the topic regardless of status.
|
|
57
|
+
|
|
58
|
+
### Test-harness fixes
|
|
59
|
+
|
|
60
|
+
- **SIGTERM port probe checks both interfaces.** The graceful-shutdown test
|
|
61
|
+
probed only `0.0.0.0`; on macOS with the framework's default `127.0.0.1`
|
|
62
|
+
listener holding the port, that bind succeeded and the pre-assertion
|
|
63
|
+
concluded the port was free. Now probes both interfaces.
|
|
64
|
+
- **MySQL connect-timeout assertion parses the reported elapsed via regex**
|
|
65
|
+
and checks it against the configured bound instead of the outer clock;
|
|
66
|
+
mysql-connector's post-abort cleanup adds ~0.7s that the two stopwatches
|
|
67
|
+
legitimately disagreed on.
|
|
68
|
+
- **AI installer tests sandbox `HOME` per-test.** `install_context()` writes
|
|
69
|
+
the global skills bundle to `Path.home()/.claude/skills` by design; in the
|
|
70
|
+
test suite that raced other tests and hit stale root-owned files from prior
|
|
71
|
+
sudo runs. `monkeypatch.setenv("HOME", ...)` redirects the global install
|
|
72
|
+
to a throwaway dir.
|
|
73
|
+
|
|
74
|
+
### Developer skills — announce and 💩
|
|
75
|
+
|
|
76
|
+
- **Announce before you act.** All four framework developer skills now
|
|
77
|
+
instruct: say what you are about to do in one line before doing it —
|
|
78
|
+
Plan / Next / Done. Never write more than two files between announcements.
|
|
79
|
+
Never run a schema migration, install a dependency, or edit the boot file
|
|
80
|
+
without a preceding "About to:" line.
|
|
81
|
+
- **💩 stale-skill detection.** All four framework developer skills gained
|
|
82
|
+
`updated_for_version: 3.13.105` in the frontmatter and a self-check that
|
|
83
|
+
compares this to the latest published skill version at
|
|
84
|
+
`https://tina4.com/skills/<name>/version` (never against the project's
|
|
85
|
+
framework version — the framework version is the developer's call). If a
|
|
86
|
+
newer skill is available, 💩 rides beside 🤖 on every reply plus a one-time
|
|
87
|
+
update instruction.
|
|
88
|
+
|
|
89
|
+
### Doc parity
|
|
90
|
+
|
|
91
|
+
- `Queue.size()`, `Queue.failed()` and `Queue.dead_letters()` gained matching
|
|
92
|
+
in-source docstrings across Python, PHP, Ruby and Node: `size("failed")` /
|
|
93
|
+
`size("dead")` / `size("dead_letter")` are aliases for the dead-letter
|
|
94
|
+
count (== length of `dead_letters()`); `failed()` lists retryable-but-
|
|
95
|
+
attempted jobs, counted under `size("pending")`, NOT `size("failed")`.
|
|
96
|
+
|
|
29
97
|
## 3.13.103
|
|
30
98
|
|
|
31
99
|
### Metrics reports what it can prove
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -1178,6 +1178,9 @@ module Tina4
|
|
|
1178
1178
|
identity = sso.is_a?(Hash) ? sso["identity"] : nil
|
|
1179
1179
|
if identity.is_a?(Hash) && identity["issuer"] && identity["subject"]
|
|
1180
1180
|
env["tina4.auth_payload"] = identity
|
|
1181
|
+
deny = rbac_forbidden(route, identity)
|
|
1182
|
+
return deny if deny
|
|
1183
|
+
|
|
1181
1184
|
return nil
|
|
1182
1185
|
end
|
|
1183
1186
|
session_token = session.get("token")
|
|
@@ -1207,9 +1210,63 @@ module Tina4
|
|
|
1207
1210
|
return [401, { "content-type" => "application/json" }, [JSON.generate({ error: "Unauthorized" })]]
|
|
1208
1211
|
end
|
|
1209
1212
|
|
|
1213
|
+
# ── RBAC guards (Feature 138): authorization AFTER authentication ──
|
|
1214
|
+
# Auth has passed (401 ruled out above). If the route carries role/
|
|
1215
|
+
# permission guards, the verified payload must satisfy them, else 403.
|
|
1216
|
+
deny = rbac_forbidden(route, env["tina4.auth_payload"])
|
|
1217
|
+
return deny if deny
|
|
1218
|
+
|
|
1210
1219
|
nil
|
|
1211
1220
|
end
|
|
1212
1221
|
|
|
1222
|
+
# Build a 403 Rack tuple when a route's RBAC guards are not satisfied by the
|
|
1223
|
+
# verified payload, else nil. AND across guard groups, OR within a group.
|
|
1224
|
+
# Roles read the `roles` claim (legacy singular `role` coerced); permissions
|
|
1225
|
+
# read `permissions` with granted-side wildcards. Feature 138 / ADR-0058.
|
|
1226
|
+
def self.rbac_forbidden(route, payload)
|
|
1227
|
+
required_roles = route.respond_to?(:roles) ? Array(route.roles) : []
|
|
1228
|
+
required_perms = route.respond_to?(:perms) ? Array(route.perms) : []
|
|
1229
|
+
return nil if required_roles.empty? && required_perms.empty?
|
|
1230
|
+
|
|
1231
|
+
subject = payload.is_a?(Hash) ? payload : {}
|
|
1232
|
+
roles = _rbac_claim_list(subject, "roles", "role")
|
|
1233
|
+
required_roles.each do |group|
|
|
1234
|
+
return _rbac_forbidden_tuple unless group.any? { |r| roles.include?(r) }
|
|
1235
|
+
end
|
|
1236
|
+
perms = _rbac_claim_list(subject, "permissions", nil)
|
|
1237
|
+
required_perms.each do |group|
|
|
1238
|
+
return _rbac_forbidden_tuple unless group.any? { |req| _rbac_perm_granted(perms, req) }
|
|
1239
|
+
end
|
|
1240
|
+
nil
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
def self._rbac_forbidden_tuple
|
|
1244
|
+
[403, { "content-type" => "application/json" }, [JSON.generate({ error: "Forbidden" })]]
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
# Read a claim as a list of strings; coerce a legacy singular string.
|
|
1248
|
+
def self._rbac_claim_list(subject, key, legacy)
|
|
1249
|
+
out = _rbac_coerce_list(subject[key])
|
|
1250
|
+
out = _rbac_coerce_list(subject[legacy]) if out.empty? && legacy
|
|
1251
|
+
out
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
def self._rbac_coerce_list(val)
|
|
1255
|
+
case val
|
|
1256
|
+
when String then val.empty? ? [] : [val]
|
|
1257
|
+
when Array then val.map(&:to_s).reject(&:empty?)
|
|
1258
|
+
else []
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
# True if any GRANTED permission satisfies the concrete REQUIRED one.
|
|
1263
|
+
# `*` grants everything; `posts.*` grants `posts.<...>` on the dot boundary.
|
|
1264
|
+
def self._rbac_perm_granted(granted, required)
|
|
1265
|
+
granted.any? do |g|
|
|
1266
|
+
g == "*" || g == required || (g.end_with?(".*") && required.start_with?(g[0..-2]))
|
|
1267
|
+
end
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1213
1270
|
def self._read_rack_body(env)
|
|
1214
1271
|
# Route through the capped reader so the running per-chunk upload cap is
|
|
1215
1272
|
# enforced on the form-token / body path too: an over-limit body raises
|
data/lib/tina4/router.rb
CHANGED
|
@@ -8,6 +8,9 @@ module Tina4
|
|
|
8
8
|
attr_reader :method, :path, :handler, :auth_handler, :swagger_meta,
|
|
9
9
|
:path_regex, :param_names, :template
|
|
10
10
|
attr_accessor :auth_required, :cached
|
|
11
|
+
# RBAC guard groups (Feature 138 / ADR-0058). Each is an array of arrays:
|
|
12
|
+
# OR within a group, AND across groups (stacking).
|
|
13
|
+
attr_reader :roles, :perms
|
|
11
14
|
|
|
12
15
|
def initialize(method, path, handler, auth_handler: nil, swagger_meta: {}, middleware: [], template: nil)
|
|
13
16
|
@method = method.to_s.upcase.freeze
|
|
@@ -23,6 +26,8 @@ module Tina4
|
|
|
23
26
|
# PY-10-02. Call .no_auth on the route to opt out explicitly.
|
|
24
27
|
@auth_required = %w[POST PUT PATCH DELETE].include?(@method)
|
|
25
28
|
@cached = false
|
|
29
|
+
@roles = []
|
|
30
|
+
@perms = []
|
|
26
31
|
@param_names = []
|
|
27
32
|
@path_regex = compile_pattern(@path)
|
|
28
33
|
@param_names.freeze
|
|
@@ -42,6 +47,31 @@ module Tina4
|
|
|
42
47
|
self
|
|
43
48
|
end
|
|
44
49
|
|
|
50
|
+
# RBAC: require ONE of the named roles (OR). Reads the verified JWT `roles`
|
|
51
|
+
# claim. Stack .role/.can for AND. Implies auth. Feature 138 / ADR-0058.
|
|
52
|
+
# Returns self for chaining: Router.get("/admin") { ... }.role("admin")
|
|
53
|
+
def role(*names)
|
|
54
|
+
names = names.flatten.map(&:to_s).reject(&:empty?)
|
|
55
|
+
unless names.empty?
|
|
56
|
+
@roles << names
|
|
57
|
+
@auth_required = true
|
|
58
|
+
end
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# RBAC: require ONE of the named permissions (OR). Reads the verified JWT
|
|
63
|
+
# `permissions` claim; granted-side wildcards (`posts.*`, `*`) satisfy a
|
|
64
|
+
# concrete requirement. Stack for AND. Implies auth. Feature 138.
|
|
65
|
+
# Returns self for chaining: Router.delete("/p") { ... }.can("posts.delete")
|
|
66
|
+
def can(*permissions)
|
|
67
|
+
permissions = permissions.flatten.map(&:to_s).reject(&:empty?)
|
|
68
|
+
unless permissions.empty?
|
|
69
|
+
@perms << permissions
|
|
70
|
+
@auth_required = true
|
|
71
|
+
end
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
45
75
|
# Mark this route as cacheable.
|
|
46
76
|
# Returns self for chaining: Router.get("/path") { ... }.cache
|
|
47
77
|
def cache
|
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.107
|
|
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-08-
|
|
11
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|