AUDIT-WP-0013-T06: independent full-scope readers may count any source (NK-WP-0035-T04)
GET /v1/reconciliation now admits may_read + tenants ["*"] identities for any source, marked basis: independent_reader. Scoped readers get 403 full_tenant_scope_required; writers keep 403 source_not_allowed. Counts only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 308701@bnt-lap001 Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
This commit is contained in:
parent
64c0fa5285
commit
d46e89ee21
6 changed files with 238 additions and 9 deletions
|
|
@ -185,7 +185,9 @@ class IngestionApplication:
|
|||
# §9.6 makes that comparison the source's own detection obligation. A
|
||||
# writer with may_read: false must therefore be able to ask, or the
|
||||
# obligation audit-core argued for is undischargeable by every sender
|
||||
# actually registered. Cross-source counts stay behind may_read.
|
||||
# actually registered. Cross-source counts stay behind may_read AND
|
||||
# full tenant scope (AUDIT-WP-0013-T06): an operator who may already
|
||||
# read every event by id may also count another source's window.
|
||||
if method == "GET" and path == "/v1/reconciliation":
|
||||
return self._reconciliation(start_response, environ, identity)
|
||||
|
||||
|
|
@ -333,12 +335,26 @@ class IngestionApplication:
|
|||
)
|
||||
|
||||
def _reconciliation(self, start_response, environ, identity):
|
||||
"""Per-class counts of one source's own events (AUDIT-WP-0009-T06).
|
||||
"""Per-class counts of one source's events (AUDIT-WP-0009-T06).
|
||||
|
||||
Counts, never payloads, and never another source's. The comparison a
|
||||
source makes against its own state transitions is the §9.6 detection
|
||||
obligation; audit-core supplies the numerator and says nothing about
|
||||
what the answer means.
|
||||
Counts, never payloads. The comparison a source makes against its own
|
||||
state transitions is the §9.6 detection obligation; audit-core
|
||||
supplies the numerator and says nothing about what the answer means.
|
||||
|
||||
Two callers are admitted, and only two (AUDIT-WP-0013-T06):
|
||||
|
||||
* the source itself — any identity whose ``sources`` permit it, with or
|
||||
without ``may_read``;
|
||||
* an independent reader — ``may_read`` with full tenant scope
|
||||
(``tenants: ["*"]``). Such an identity can already read every stored
|
||||
event by id or correlation, so per-class counts tell it strictly
|
||||
less than it may read. A tenant-scoped reader is refused: counting
|
||||
another source's whole window enumerates existence beyond what an
|
||||
id lookup filtered to its tenants discloses. The response carries
|
||||
``basis: "independent_reader"`` so a receipt cannot be mistaken for
|
||||
the source's own reconciliation.
|
||||
|
||||
A writer without ``may_read`` still may not count another source.
|
||||
"""
|
||||
query = parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True)
|
||||
source = (query.get("source") or [""])[0]
|
||||
|
|
@ -346,13 +362,19 @@ class IngestionApplication:
|
|||
return self._json(
|
||||
start_response, HTTPStatus.BAD_REQUEST, {"error": "source_required"}
|
||||
)
|
||||
if not identity.permits_source(source):
|
||||
independent = not identity.permits_source(source)
|
||||
if independent and not identity.may_read:
|
||||
# 403 rather than an empty count. A zero here would read as "we
|
||||
# hold none of yours", which is a materially different and false
|
||||
# answer to a question about completeness.
|
||||
return self._json(
|
||||
start_response, HTTPStatus.FORBIDDEN, {"error": "source_not_allowed"}
|
||||
)
|
||||
if independent and not identity.has_full_tenant_scope():
|
||||
return self._json(
|
||||
start_response, HTTPStatus.FORBIDDEN,
|
||||
{"error": "full_tenant_scope_required"},
|
||||
)
|
||||
window = _window(query)
|
||||
if window is None:
|
||||
return self._json(
|
||||
|
|
@ -402,6 +424,7 @@ class IngestionApplication:
|
|||
"until": until,
|
||||
"counts": counts,
|
||||
**({"stream_instance_id": stream_instance_id} if instances else {}),
|
||||
**({"basis": "independent_reader"} if independent else {}),
|
||||
# Said in the response because this is the number most likely to
|
||||
# be quoted out of context in someone else's conformance argument.
|
||||
"means": (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue