7.4 Enforcement
The order in which eventd evaluates a query is fixed (PSPU §3.18), and access control is the third phase — before predicates, transforms, grouping, aggregation, ordering and pagination. What follows is the sequence within that phase.
- Obtain the caller's token from the connection (§7.1). Failure denies the query.
- Parse the query to establish its data sources and filters (§6.1).
- Discover the concrete identifiers the query could touch — event
type strings, log origin strings, metric name strings.
A broad
selector authorizes nothing by itself:
EVENTS,EVENTS kacs.*,LOGSwithoutFROMandMETRIC cpu.*are each resolved identifier by identifier. - Resolve and check each discovered identifier: find its descriptor
by hierarchical matching (§7.2), and build the object type list for
the fields the query references (§7.3). A query that references none
asks instead about every field the descriptor's allowing object ACEs
name, since those may make its records visible though the query names
none of them; a GUID does not say its path, so each is a level-1 node
of that list. Call
kacs_access_check_list, and cache the verdict for this(token, identifier, field set)(§7.5). - Apply record verdicts. An identifier is invisible when the caller may read neither its root nor any field: its records are excluded from the logical row set before aggregation, ordering, pagination and formatting. One whose root is denied but some field granted stays, for its records to be shaped to those fields (step 10). For a cross-type source, a denied identifier is treated as having no matching data.
- Apply field verdicts to predicates. Where the query references a field in a predicate or shaping clause and a matching identifier does not grant it, that identifier's records contribute nothing — exactly as if its root had been denied. The query is not rejected.
- Cross-type sources get the same treatment. An invisible identifier, or a denied field needed to evaluate the condition, makes the condition evaluate as though no matching cross-source data existed.
- Execute, with root filtering already part of the logical row set.
- Re-resolve per result identifier. For each distinct concrete
identifier in the resulting rows, resolve its descriptor, build the
object type list with field GUIDs, call
kacs_access_check_listwith the token, the descriptor,EVENTD_READ, the list and an audit context naming the pattern the descriptor was resolved from (below), and cache the per-field results. - Shape each record. Look up the cached results for its identifier and fields. Exclude the record entirely if neither the root nor any of its fields was granted; otherwise include it, with each field only if its node was granted. A grant of the root alone grants every field, since an ACE with no object type applies to the whole tree (§7.3). A grant of some fields alone gives records holding exactly those fields, and a record carrying none of them is excluded.
7.4.1 Which clauses count as referencing a field #
Step 6 applies to every clause that reads a value rather than merely displaying one:
- ordinary
WHEREpredicates - metric label filters in a primary selector
GROUP,COUNT BY,TOP N BY,SORTandDISTINCTfields- event and log aggregation arguments —
SUM,AVG,MIN,MAX - metric transforms and terminal aggregations, all of which read the
fixed
valuefield:RATE,DELTA,P50,P95,P99,AVG,MIN,MAX,SUM,AVG_OVER,MIN_OVER,MAX_OVER,SUM_OVER - a metric boot filter, which reads
boot_id; and an explicit metric type predicate, grouping, sort or distinct, which readstype
SELECT is not on this list.
It shapes output
and is applied last, so a
field it omits was still available to every earlier phase — and
conversely, selecting a field the caller may not read removes the field,
not the record.
7.4.2 Denial is silent, not fatal #
Step 6 excludes rather than rejects, and this is the decision most worth being explicit about, because rejecting is the more informative behaviour and that is exactly the objection to it.
A rejection would tell the caller that some identifier exists, matches its query, and carries a field it may not read — three facts about data it was not permitted to see, delivered by the mechanism meant to withhold them, and enumerable by trying queries and watching which ones fail. Excluding costs the caller a result narrower than it appears; rejecting costs the model the property it rests on.
Cross-source fields already worked this way (step 7); primary-source fields now match them.
7.4.3 Field authorization does not depend on presence #
A field's authorization is resolved from the name as written, against each concrete identifier, whether or not any record of that identifier actually carries it.
Payload fields vary between records of the same type, so a rule turning on presence would require the scan that authorization is meant to precede.
7.4.4 Internal values are not fields #
Row identifiers, series identifiers, ordering tiebreakers and series type checks are not query-language source fields unless the mode exposes them as fixed fields or the query names them (§6.2). Errors raised by internal checks never carry a denied field's value.
A metric result's value is a source field, because it is either a
raw sample or a scalar derived from raw samples.
7.4.5 Filtering is part of the logical result #
Access filtering is not a presentation step. Aggregating, ordering or paginating over unreadable records would leak them through counts, through ordering, and through the gaps in pagination.
eventd may push authorization predicates into SQL when the identifier
set is known, or read candidates and filter them before aggregating
(§6.3). The externally visible result is identical to filtering first,
and COUNT, COUNT BY, TOP N BY and every other aggregate reflect
only what the caller may see.
7.4.6 The audit trail #
Whether an access check produces a KACS audit event is decided by the
SACL audit walk in the AccessCheck pipeline, against the descriptor's
SACL. Every default descriptor eventd creates carries a failure-audit
ACE for Everyone at every eventd right (§7.2), so out of the box every
denied check produces a kacs.audit.access.checked record, and a
granted one produces none unless an administrator adds a success ACE.
KACS writes such a record from the AccessCheck syscall only when the
caller holds SeAuditPrivilege enabled. eventd's service definition
requires that privilege, and its token holds it enabled; without it the
check would be made and nothing recorded.
eventd passes each check an audit context naming the object it guards, in the form PGSS §6.7 defines: a MessagePack map of the object's kind and, under the kind's own name, the pattern whose descriptor was checked.
| Check | Audit context |
|---|---|
| Reading events | {kind: "event-namespace", event-namespace: {pattern: "kacs"}} |
| Reading logs | {kind: "log-namespace", log-namespace: {pattern: "loregd"}} |
| Reading or publishing metrics | {kind: "metric-namespace", metric-namespace: {pattern: "cpu"}} |
INDEX (§7.2) | {kind: "eventd-admin"} |
The pattern is the one the descriptor was found under (§7.2), not the
identifier asked about, and for a log origin it never holds the producer
after a /. KACS copies the map into its kacs.audit.access.checked
record as object.kind and object.<kind>.pattern, and marks the
record with fields.attestation.userspace, since those values are
eventd's claim. The audit trail therefore records exactly which
observability data was read, by whom — the client, as subject.* —
rather than merely that eventd performed a check. Reading and
publishing a metric name the same object; the record's
access.requested says which was checked.
Those audit events are themselves KMES events, which eventd consumes and
stores, and which are governed by the event patterns like any other,
under kacs (§7.2).
Reading the audit store is auditable.