forest_admin_agent 1.44.1 → 1.44.2
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb144d36d50f4763b0b8f2e61e701ee5fbfb7a97b66b4a9a6f67aaff26b8ac5d
|
|
4
|
+
data.tar.gz: c6f7817f4a8507b703c604b8c4c643884eb57b8d0902cd90437e6ffe1b201238
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c76840f3a4d7823a6395b6926e5ba60cb018f8acc30f6166773334ce6e966292b410001ad262a26cf0ca36580bf08dfcd0c0451821f52552919c37ec242083a7
|
|
7
|
+
data.tar.gz: fd08b9086757654dd821a909376648fa0f1a7274b18118cf734573bfc99f1818fabc063f085c72d1bccd7d94a8852f299aa0cd23703a3588d55f81a33a12f03b
|
data/AUDIT_TRAIL.md
CHANGED
|
@@ -199,8 +199,21 @@ untouched. The row itself is always returned: what happened, by whom and when st
|
|
|
199
199
|
|
|
200
200
|
A snapshot only answers for the writable columns it captured, so a scope reaching for anything else — a
|
|
201
201
|
read-only column, a relation, a value stored redacted — is not evaluated against it at all and the values
|
|
202
|
-
are withheld: `nil` there is a missing answer, not a passing one
|
|
203
|
-
|
|
202
|
+
are withheld: `nil` there is a missing answer, not a passing one, and a redacted value answers no better.
|
|
203
|
+
Primary keys are the exception: the packed id fills in whatever the snapshot cannot answer — a read-only key
|
|
204
|
+
it never captured, or a writable one the trail redacts — so a scope on the id still matches the record it
|
|
205
|
+
belongs to. It fills in only what is missing: where a side captured the key itself, that value is the one
|
|
206
|
+
that was true there. Each side of an update is read against the id it was filed under, the row's own id and,
|
|
207
|
+
for the previous side of an update that moved the key, the id it moved from. A `pending` row is filed under
|
|
208
|
+
the id the record had *before* the write, which may not have landed, so its new side is given no id to fill
|
|
209
|
+
from and answers with what it captured or not at all.
|
|
210
|
+
|
|
211
|
+
Neither failure this can meet is fatal, and each costs only what it actually broke. An id that stopped
|
|
212
|
+
decoding when the primary key changed shape costs the keys it would have filled and nothing more, so a scope
|
|
213
|
+
that never asks about the id is still answered from the columns the row captured. A column captured as `nil`
|
|
214
|
+
that an ordered operator cannot compare withholds that row. Uncaught, either would have failed the whole
|
|
215
|
+
page, and only for the callers a scope applies to, since an unscoped caller never reaches this test. Both
|
|
216
|
+
are logged, and the row keeps its operation, author and timestamp. This is also
|
|
204
217
|
what makes an update's partial diff safe to test: a diff that never carried the scoped column answers for
|
|
205
218
|
neither side, so both are withheld.
|
|
206
219
|
|
|
@@ -114,9 +114,9 @@ module ForestAdminAgent
|
|
|
114
114
|
case entry.operation
|
|
115
115
|
# `delete`'s previous_values and `create`'s new_values both capture every writable column.
|
|
116
116
|
when 'delete'
|
|
117
|
-
in_scope?(entry, entry.
|
|
117
|
+
in_scope?(entry.previous_values, entry.record_id, scope, context) ? entry : blank(entry, :previous_values)
|
|
118
118
|
when 'create'
|
|
119
|
-
in_scope?(entry, entry.
|
|
119
|
+
in_scope?(entry.new_values, entry.record_id, scope, context) ? entry : blank(entry, :new_values)
|
|
120
120
|
when 'update'
|
|
121
121
|
withhold_each_side(entry, scope, context)
|
|
122
122
|
# `action`/`action_failed` rows hold a submitted form and a result summary, not column values, so
|
|
@@ -131,30 +131,61 @@ module ForestAdminAgent
|
|
|
131
131
|
# separately releases the ones that can be proven in scope — "it used to be X" can't escape through a
|
|
132
132
|
# row whose new value is out of scope, since that side is tested on its own.
|
|
133
133
|
def withhold_each_side(entry, scope, context)
|
|
134
|
-
|
|
134
|
+
# An update that moved a writable primary key files its row under the id the record ended up with,
|
|
135
|
+
# and keeps the one it had on `previous_record_id`. Each side is tested against the id it was true
|
|
136
|
+
# of, or the new state's id would decide whether the old state is in scope.
|
|
137
|
+
before = entry.previous_record_id || entry.record_id
|
|
138
|
+
kept = in_scope?(entry.previous_values, before, scope, context) ? entry : blank(entry, :previous_values)
|
|
135
139
|
|
|
136
|
-
in_scope?(kept, kept
|
|
140
|
+
in_scope?(kept.new_values, after_id(kept), scope, context) ? kept : blank(kept, :new_values)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A pending row is filed under the id the record had *before* the write, since the write may not have
|
|
144
|
+
# landed: it says nothing about the state the update was moving to, so the new side gets no id to fill
|
|
145
|
+
# from and falls back on what it captured itself.
|
|
146
|
+
def after_id(entry)
|
|
147
|
+
entry.status == ::ForestAdminAgent::AuditTrail::Recording::PENDING ? nil : entry.record_id
|
|
137
148
|
end
|
|
138
149
|
|
|
139
150
|
# Only a snapshot that answers every field the scope asks about, with what was really stored, is worth
|
|
140
151
|
# matching. The capture keeps the writable columns, so a scope on anything else — a read-only column, a
|
|
141
152
|
# relation — reads as nil there and would answer for a value the row never held: `status != 'private'`
|
|
142
153
|
# would match, and an ordered operator would raise on the nil. A redacted value answers no better.
|
|
143
|
-
def in_scope?(
|
|
144
|
-
snapshot =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
def in_scope?(values, packed_id, scope, context)
|
|
155
|
+
snapshot = answerable_snapshot(values, packed_id, context.collection)
|
|
156
|
+
return false unless scope.projection.all? { |field| snapshot.key?(field) }
|
|
157
|
+
|
|
158
|
+
scope.match(snapshot, context.collection, context.caller.timezone)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
# Key presence is not answerability: a column captured as nil has its key, and an ordered operator
|
|
161
|
+
# raises on it. Uncaught that would fail the whole page, and only for the callers a scope applies
|
|
162
|
+
# to. One withheld row is the smaller loss, and the same answer the field would have got had it
|
|
163
|
+
# been missing outright.
|
|
164
|
+
Facades::Container.logger&.log('Warn', "[ForestAdmin] Audit row not scope-checkable: #{e.message}")
|
|
165
|
+
|
|
166
|
+
false
|
|
150
167
|
end
|
|
151
168
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
# What this side of the row can answer about. A redacted value answers nothing, so it is dropped rather
|
|
170
|
+
# than matched against the placeholder — leaving the field unanswered, which withholds. The packed id
|
|
171
|
+
# then fills in the primary keys: a read-only one never lands in the snapshot at all, and a writable one
|
|
172
|
+
# the trail redacts was just dropped, while the id the row was filed under proves what the key was.
|
|
173
|
+
# It only fills what the snapshot cannot answer: on the side of a row that captured the key itself,
|
|
174
|
+
# that value is the one that was true there.
|
|
175
|
+
def answerable_snapshot(values, packed_id, collection)
|
|
176
|
+
answered = (values || {}).reject { |_, value| value == ::ForestAdminAgent::AuditTrail::Recording::REDACTED }
|
|
177
|
+
return answered if packed_id.nil?
|
|
156
178
|
|
|
157
|
-
|
|
179
|
+
begin
|
|
180
|
+
Utils::Id.unpack_id(collection, packed_id, with_key: true).merge(answered)
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
# An id written under a primary key of another shape costs the keys it would have filled, and
|
|
183
|
+
# nothing else: the snapshot still answers for the columns it captured, so a scope that never
|
|
184
|
+
# asks about the id is unaffected.
|
|
185
|
+
Facades::Container.logger&.log('Warn', "[ForestAdmin] Audit row id not decodable: #{e.message}")
|
|
186
|
+
|
|
187
|
+
answered
|
|
188
|
+
end
|
|
158
189
|
end
|
|
159
190
|
|
|
160
191
|
def blank(entry, *fields)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_admin_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.44.
|
|
4
|
+
version: 1.44.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthieu
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-09-
|
|
12
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|