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.

  1. Obtain the caller's token from the connection (§7.1). Failure denies the query.
  2. Parse the query to establish its data sources and filters (§6.1).
  3. 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.*, LOGS without FROM and METRIC cpu.* are each resolved identifier by identifier.
  4. 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).
  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.
  6. 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.
  7. 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.
  8. Execute, with root filtering already part of the logical row set.
  9. 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_list with 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.
  10. 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 WHERE predicates
  • metric label filters in a primary selector
  • GROUP, COUNT BY, TOP N BY, SORT and DISTINCT fields
  • event and log aggregation arguments — SUM, AVG, MIN, MAX
  • metric transforms and terminal aggregations, all of which read the fixed value field: 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 reads type

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.

CheckAudit 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.

Edit this page