LCOV - code coverage report
Current view: top level - antares-api/src - policy.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 97.2 % 2117 2057
Test Date: 2026-11-02 13:21:27 Functions: 41.2 % 1200 495

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The policy seam: one trait, one built-in engine, every engine an addon
       3              : //! (ADR-0020).
       4              : //!
       5              : //! The broker takes no authorization decision of its own. It asks the
       6              : //! engine a deployment gave it and obeys, and what an engine may answer is
       7              : //! deliberately narrow: allow, deny, or narrow the operation. Nothing here
       8              : //! parses a credential or validates a token — authentication, rate limiting
       9              : //! and quotas stay in the gateway in front of the broker.
      10              : //!
      11              : //! Everything in this module is core code, compiled and tested in every
      12              : //! build. The only engine the broker ships is [`AllowAll`], and conformance
      13              : //! is asserted against it; any other engine is an addon crate outside
      14              : //! `crates/`, behind an off-by-default `antares-broker` feature.
      15              : //!
      16              : //! The seam fails closed: an engine that errors, panics or runs past
      17              : //! [`TIMEOUT`] denies. A deployment that wires in a broken engine loses
      18              : //! service, never its access rules.
      19              : 
      20              : use antares_ql::geo::GeoQuery;
      21              : use antares_ql::QNode;
      22              : use axum::http::HeaderMap;
      23              : use serde_json::Value;
      24              : use std::borrow::Cow;
      25              : use std::collections::BTreeMap;
      26              : use std::future::Future;
      27              : use std::pin::Pin;
      28              : use std::sync::LazyLock;
      29              : 
      30              : /// The `@context` [`Filter::project`] reads rule conditions with.
      31              : static CORE_CONTEXT: LazyLock<antares_jsonld::Context> =
      32              :     LazyLock::new(antares_jsonld::core_context);
      33              : use std::time::Duration;
      34              : 
      35              : /// What [`PolicyEngine::decide`] hands back. Boxed because an engine that
      36              : /// asks a policy server has to await, and the trait must stay object-safe:
      37              : /// the broker holds one `Arc<dyn PolicyEngine>` chosen at startup.
      38              : pub type DecisionFuture<'a> = Pin<Box<dyn Future<Output = Decision> + Send + 'a>>;
      39              : 
      40              : /// What [`PolicyEngine::decide_each`] hands back: one [`Decision`] per
      41              : /// Operation it was given, in the same order.
      42              : pub type DecisionsFuture<'a> = Pin<Box<dyn Future<Output = Vec<Decision>> + Send + 'a>>;
      43              : 
      44              : /// How long an engine has to answer before the broker stops waiting and
      45              : /// denies. Deployment knob (`ANTARES_POLICY_TIMEOUT_MS`), read once at
      46              : /// first use. The default is short on purpose: the seam sits in front of
      47              : /// every request, and an engine that cannot answer inside it is an outage
      48              : /// either way — failing closed at 250 ms is the difference between a 403
      49              : /// and a broker that stops accepting.
      50           38 : pub static TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
      51           38 :     let env = std::env::var("ANTARES_POLICY_TIMEOUT_MS").ok();
      52           38 :     Duration::from_millis(timeout_ms(env.as_deref()))
      53           38 : });
      54              : 
      55              : /// `ANTARES_POLICY_TIMEOUT_MS` in milliseconds: a positive integer, else 250.
      56           58 : fn timeout_ms(env: Option<&str>) -> u64 {
      57           58 :     env.and_then(|v| v.parse().ok())
      58           58 :         .filter(|n: &u64| *n > 0)
      59           58 :         .unwrap_or(250)
      60           58 : }
      61              : 
      62              : /// The reasons the seam denies on its own, rather than on an engine's word.
      63              : /// Fixed strings: they reach the client in a ProblemDetails `detail`, and
      64              : /// an engine's own text about why it failed is not the client's business.
      65              : pub const ENGINE_TIMED_OUT: &str = "the policy engine did not answer in time";
      66              : pub const ENGINE_FAILED: &str = "the policy engine failed";
      67              : 
      68              : /// The clauses whose operations act on everything the tenant holds. There
      69              : /// is no narrowed form of "delete every entity" or "snapshot the tenant",
      70              : /// so a [`Decision::Filter`] on one of them is answered as a deny
      71              : /// ([`resolve`]).
      72              : pub const WHOLE_TENANT: [&str; 4] = ["5.6.21", "5.16.1", "5.16.2", "5.16.7"];
      73              : 
      74              : /// The clauses whose handlers read the [`Filter`] an engine returns: the
      75              : /// reads that can serve less than they were asked for. Every other clause
      76              : /// takes its operation whole — a create either writes the Entity or does
      77              : /// not — so a [`Decision::Filter`] there would be dropped, and the broker
      78              : /// would perform in full an operation the engine believed it had narrowed.
      79              : /// That is the one direction the seam may not fail in, so those clauses
      80              : /// answer a narrowing as a deny ([`resolve`]).
      81              : /// Discovery (5.7.5 to 5.7.10) is among them: the narrowing rules compute it
      82              : /// from the Entities the narrowing selects.
      83              : /// Query Subscriptions (5.8.4, 5.11.5) and Query Registrations (5.10.2)
      84              : /// narrow the list ([`RECORD_LISTS`]).
      85              : pub const FILTERABLE: [&str; 15] = [
      86              :     "5.7.1", "5.7.2", "5.7.3", "5.7.4", "5.7.5", "5.7.6", "5.7.7", "5.7.8", "5.7.9", "5.7.10",
      87              :     "5.14.4", "5.14.5", "5.8.4", "5.11.5", "5.10.2",
      88              : ];
      89              : 
      90              : /// The FILTERABLE clauses that list Subscriptions or Registrations. A
      91              : /// narrowing there selects which documents are listed, counted and paged,
      92              : /// and projects nothing: those are the client's own 5.2.12 and 5.2.9
      93              : /// records, not Entities with Attributes to drop, so a [`Filter`] that
      94              : /// projects is a deny there ([`resolve`]).
      95              : pub const RECORD_LISTS: [&str; 3] = ["5.8.4", "5.11.5", "5.10.2"];
      96              : 
      97              : /// List and Serve @context (5.13.3, 5.13.4). A stored `@context` may define
      98              : /// a term for an Attribute the caller may not see, so a narrowing there,
      99              : /// whatever it holds, says the caller sees fewer names than the tenant
     100              : /// holds: the list is the core `@context` alone and a stored one is not
     101              : /// found. [`resolve`] passes it through.
     102              : pub const NAME_READS: [&str; 2] = ["5.13.3", "5.13.4"];
     103              : 
     104              : /// Who is asking. The headers are the ones a deployment names for the
     105              : /// seam to carry, copied verbatim and never interpreted: the broker does
     106              : /// not know what a token is. They never leave this process — stripped from every
     107              : /// forwarded request, absent from notifications, dead letters and logs,
     108              : /// which is why [`std::fmt::Debug`] here prints names and not values.
     109              : #[derive(Clone)]
     110              : pub struct Subject {
     111              :     pub tenant: antares_model::TenantId,
     112              :     pub headers: Vec<(String, String)>,
     113              : }
     114              : 
     115              : /// The name of the reserved pair in [`Subject::headers`] that says which view
     116              : /// the request came through (ADR-0020 amendment). `@` is no
     117              : /// header-name character (RFC 9110 `tchar`), so no request can carry it:
     118              : /// only [`through_view`] puts it there.
     119              : pub const VIEW_PAIR: &str = "@view";
     120              : 
     121              : tokio::task_local! {
     122              :     /// The view the in-process request running in this task came through.
     123              :     static VIEW: std::sync::Arc<str>;
     124              : }
     125              : 
     126              : /// Runs `f` as a request through the view `id`: every [`Subject`] built in
     127              : /// it carries the [`VIEW_PAIR`]. The view surfaces' operation layer is the
     128              : /// one caller; a request off the socket never runs inside it.
     129           12 : pub async fn through_view<F: Future>(id: &str, f: F) -> F::Output {
     130           12 :     VIEW.scope(std::sync::Arc::from(id), f).await
     131           12 : }
     132              : 
     133              : impl Subject {
     134              :     /// The view the request came through, if it came through one.
     135              :     #[must_use]
     136           16 :     pub fn view(&self) -> Option<&str> {
     137           16 :         self.headers
     138           16 :             .iter()
     139           16 :             .find(|(name, _)| name == VIEW_PAIR)
     140           16 :             .map(|(_, id)| id.as_str())
     141           16 :     }
     142              : }
     143              : 
     144              : impl std::fmt::Debug for Subject {
     145            4 :     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     146            4 :         f.debug_struct("Subject")
     147            4 :             .field("tenant", &self.tenant)
     148            4 :             .field(
     149            4 :                 "headers",
     150            4 :                 &self.headers.iter().map(|(k, _)| k).collect::<Vec<_>>(),
     151              :             )
     152            4 :             .finish()
     153            4 :     }
     154              : }
     155              : 
     156              : /// What is being asked for, after negotiation and expansion: the request
     157              : /// the broker is about to run, in the terms the operation itself uses.
     158              : /// Every name is expanded, so an engine writes its rules against IRIs and
     159              : /// not against whatever short name the caller's `@context` happened to use.
     160              : pub struct Operation<'a> {
     161              :     /// The CIM 009 clause of the operation, e.g. `"5.6.1"`.
     162              :     pub clause: &'static str,
     163              :     /// The Entity identifiers the operation names, as the handler holds
     164              :     /// them; empty for a query that names none.
     165              :     pub ids: &'a [&'a str],
     166              :     pub types: &'a [String],
     167              :     pub attrs: &'a [String],
     168              :     pub q: Option<&'a QNode>,
     169              :     pub scope_q: Option<&'a str>,
     170              :     pub geo: Option<&'a GeoQuery>,
     171              :     /// The request body of a write, expanded. `None` for a read.
     172              :     pub body: Option<&'a Value>,
     173              :     /// What a write by id lands on, as stored: `None`
     174              :     /// for a read, a create, or an id the tenant does not hold.
     175              :     pub stored: Option<&'a StoredImage>,
     176              : }
     177              : 
     178              : /// The stored Entity a write by id changes, as an engine judges it
     179              : /// (operation matrix item 2): what the Entity is,
     180              : /// not what the request says it is. Names only, expanded; never a value.
     181              : #[derive(Debug, Clone, Default, PartialEq, Eq)]
     182              : pub struct StoredImage {
     183              :     pub types: Vec<String>,
     184              :     pub scope: Vec<String>,
     185              :     pub attribute_names: Vec<String>,
     186              : }
     187              : 
     188              : impl StoredImage {
     189              :     /// The image of one stored, expanded Entity document.
     190          286 :     pub fn of(doc: &Value) -> Self {
     191          332 :         let strings = |v: Option<&Value>| match v {
     192            8 :             Some(Value::String(s)) => vec![s.clone()],
     193          324 :             Some(Value::Array(a)) => a
     194          324 :                 .iter()
     195          324 :                 .filter_map(Value::as_str)
     196          324 :                 .map(str::to_owned)
     197          324 :                 .collect(),
     198          240 :             _ => Vec::new(),
     199          572 :         };
     200              :         Self {
     201          286 :             types: strings(doc.get("type")),
     202          286 :             scope: strings(doc.get("scope")),
     203          286 :             attribute_names: doc
     204          286 :                 .as_object()
     205          286 :                 .map(|o| {
     206          286 :                     o.keys()
     207         1648 :                         .filter(|k| {
     208         1648 :                             !k.starts_with("__")
     209         1644 :                                 && !k.starts_with('@')
     210         1162 :                                 && !matches!(
     211         1640 :                                     k.as_str(),
     212         1640 :                                     "id" | "type"
     213         1076 :                                         | "scope"
     214         1030 :                                         | "createdAt"
     215          756 :                                         | "modifiedAt"
     216          482 :                                         | "deletedAt"
     217              :                                 )
     218         1648 :                         })
     219          286 :                         .cloned()
     220          286 :                         .collect()
     221          286 :                 })
     222          286 :                 .unwrap_or_default(),
     223              :         }
     224          286 :     }
     225              : }
     226              : 
     227              : /// Shape and counts, never the payload: an `Operation` carries the
     228              : /// caller's data, and a log line is not where it belongs.
     229              : impl std::fmt::Debug for Operation<'_> {
     230            8 :     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     231            8 :         f.debug_struct("Operation")
     232            8 :             .field("clause", &self.clause)
     233            8 :             .field("ids", &self.ids.len())
     234            8 :             .field("types", &self.types.len())
     235            8 :             .field("attrs", &self.attrs.len())
     236            8 :             .field("q", &self.q.is_some())
     237            8 :             .field("scope_q", &self.scope_q.is_some())
     238            8 :             .field("geo", &self.geo.is_some())
     239            8 :             .field("body", &self.body.is_some())
     240            8 :             .field("stored", &self.stored.is_some())
     241            8 :             .finish()
     242            8 :     }
     243              : }
     244              : 
     245              : impl Operation<'_> {
     246              :     /// The operation with nothing but its clause: what a handler that
     247              :     /// carries no ids, types or attributes passes, and the base every other
     248              :     /// call site fills in with struct-update syntax.
     249         3836 :     pub const fn new(clause: &'static str) -> Operation<'static> {
     250         3836 :         Operation {
     251         3836 :             clause,
     252         3836 :             ids: &[],
     253         3836 :             types: &[],
     254         3836 :             attrs: &[],
     255         3836 :             q: None,
     256         3836 :             scope_q: None,
     257         3836 :             geo: None,
     258         3836 :             body: None,
     259         3836 :             stored: None,
     260         3836 :         }
     261         3836 :     }
     262              : }
     263              : 
     264              : /// What an engine may answer: an operation is allowed, refused, allowed
     265              : /// over less than it asked for, or allowed on a condition the stored Entity
     266              : /// has to meet.
     267              : #[derive(Debug, Clone, PartialEq)]
     268              : pub enum Decision {
     269              :     Allow,
     270              :     /// Refused, with the engine's own reason. Answered 403 with a
     271              :     /// ProblemDetails whose type is an Antares URI — Table 6.3.2-1 names no
     272              :     /// access-denied error, so this is an Antares decision.
     273              :     Deny(String),
     274              :     /// Allowed over less. The caller cannot tell a hidden entity from an
     275              :     /// absent one.
     276              :     Filter(Filter),
     277              :     /// Allowed only if the stored Entity the write lands on is one the
     278              :     /// filter selects: its `q`, `scopeQ`, areas and the types and
     279              :     /// conditions of its `rules`, judged on stored values, which the
     280              :     /// [`Operation`] does not carry. The store judges the document it is
     281              :     /// about to change, inside the write's own read-modify-write, so a
     282              :     /// concurrent write between this answer and the commit cannot carry the
     283              :     /// write onto an Entity the filter no longer selects. An Entity it does
     284              :     /// not select answers as an absent one. Honoured on [`WRITE_BY_ID`], with
     285              :     /// a filter that selects and projects nothing; anywhere else a deny
     286              :     /// ([`resolve`]).
     287              :     AllowIf(Filter),
     288              : }
     289              : 
     290              : /// The writes by id a [`Decision::AllowIf`] can condition: each changes or
     291              : /// deletes Entities that are already stored, by id, and judges each of them
     292              : /// inside its store operation. A create has no stored Entity to judge; Batch
     293              : /// Upsert updates the members its condition selects and creates none of the
     294              : /// conditioned ones. The temporal writes 5.6.12 to 5.6.15 judge the
     295              : /// evolution's latest instances (`Condition::pre_evolution`); 5.6.16 is
     296              : /// not one: the temporal seam has no delete that reads the document first.
     297              : pub const WRITE_BY_ID: [&str; 16] = [
     298              :     "5.6.2", "5.6.3", "5.6.4", "5.6.5", "5.6.6", "5.6.17", "5.6.18", "5.6.19", "5.6.8", "5.6.9",
     299              :     "5.6.10", "5.6.20", "5.6.12", "5.6.13", "5.6.14", "5.6.15",
     300              : ];
     301              : 
     302              : /// Why a write conditioned on its stored Entity is refused rather than
     303              : /// forwarded: a Context Source's copy is not in this store, so nothing here
     304              : /// can judge it.
     305              : pub const CONDITION_NOT_FORWARDED: &str =
     306              :     "a write conditioned on the stored Entity is not forwarded to a Context Source";
     307              : 
     308              : /// How much less. Every member narrows and none widens: the `q`, `scopeQ`
     309              : /// and `geo_q` are conjoined with the caller's own, `pick` keeps a subset of
     310              : /// the members, `omit` removes some.
     311              : #[derive(Debug, Clone, Default, PartialEq)]
     312              : pub struct Filter {
     313              :     /// Conjoined into the query the store runs, on the AST — never on the
     314              :     /// query string, where the 4.9 precedence of `;` against `|` would have
     315              :     /// to be re-derived by whoever writes the rule. Boxed, like `geo_q`: a
     316              :     /// parsed query is half the size of the struct, and every [`Decision`]
     317              :     /// and [`NotifyDecision`] would carry it inline.
     318              :     pub q: Option<Box<QNode>>,
     319              :     /// Conjoined the same way with the request's own `scopeQ` (4.18).
     320              :     pub scope_q: Option<String>,
     321              :     /// An area the Entity must lie in (4.10), held beside the request's own
     322              :     /// geoquery and never in its place: 7.2.5 lets a request carry one, so
     323              :     /// the store query takes both and every one must hold.
     324              :     /// A [`GeoQuery`] is only built by `GeoQuery::from_params`, which bounds
     325              :     /// it like a request's. Boxed: a parsed geometry is large, and every
     326              :     /// [`Decision`] would carry its size.
     327              :     pub geo_q: Option<Box<GeoQuery>>,
     328              :     /// Further areas, every one of which must hold as well: a view and an
     329              :     /// engine that both narrow by area compose into `geo_q` and these.
     330              :     /// An engine with one area leaves this empty; read
     331              :     /// them all through [`Filter::areas`].
     332              :     pub geo_also: Vec<GeoQuery>,
     333              :     /// Members removed from every document served.
     334              :     pub omit: Vec<String>,
     335              :     /// If non-empty, the only members served beside the document frame.
     336              :     pub pick: Vec<String>,
     337              :     /// Attributes allowed per rule: when
     338              :     /// non-empty, an Entity is served only if at least one [`Rule`] holds
     339              :     /// for it, and it keeps the union of the `attributes` of the rules that
     340              :     /// hold, nothing else beside its frame. An Entity no rule holds for is
     341              :     /// left out of the answer. Applied to every document the answer
     342              :     /// carries, joined targets and notified Entities included, each by its
     343              :     /// own rules. It intersects with `pick`: both narrow.
     344              :     pub rules: Vec<Rule>,
     345              :     /// Attributes removed per Entity type: an Entity loses every Attribute
     346              :     /// any of its types omits.
     347              :     pub omit_by_type: BTreeMap<String, Vec<String>>,
     348              :     /// Answer [`RESTRICTED_HEADER`]`: true`, so a client can know the
     349              :     /// answer was narrowed. Narrowing is otherwise silent.
     350              :     pub restricted: bool,
     351              :     /// The engine's advisory, sent as `NGSILD-Warning` (TS 104 176 6.3.17):
     352              :     /// e.g. the Entity types a filter on an Attribute the caller
     353              :     /// may not filter on left out, so a developer can tell that refusal
     354              :     /// from an empty answer. The text is the engine's, never an echo of the
     355              :     /// request, and names only what the caller may already see.
     356              :     pub warning: Option<Warning>,
     357              :     /// Windows of `observedAt` the instances of a temporal read (5.7.3,
     358              :     /// 5.7.4) must fall into, every one of them: the request's own
     359              :     /// 4.11 window is clamped to them before the store runs, so the store
     360              :     /// fetches no instance outside them. A window the request does not reach
     361              :     /// answers nothing, never a refusal that tells where the grant ends. A
     362              :     /// slice, not a `Vec`: every [`Decision`] carries a `Filter`'s size.
     363              :     pub temporal: Box<[Window]>,
     364              : }
     365              : 
     366              : /// A half-open interval `[from, to)` of `observedAt`; an open end is the
     367              : /// beginning or the end of time.
     368              : #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
     369              : pub struct Window {
     370              :     /// The earliest instant admitted.
     371              :     pub from: Option<TimeBound>,
     372              :     /// The first instant no longer admitted.
     373              :     pub to: Option<TimeBound>,
     374              : }
     375              : 
     376              : /// One end of a [`Window`]: a fixed instant, or a duration before the
     377              : /// moment the request is decided, so "the last 24 hours" slides with now.
     378              : #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     379              : pub enum TimeBound {
     380              :     /// This instant.
     381              :     At(chrono::DateTime<chrono::Utc>),
     382              :     /// This long before now.
     383              :     Ago(chrono::Duration),
     384              : }
     385              : 
     386              : impl TimeBound {
     387         2648 :     fn at(self, now: chrono::DateTime<chrono::Utc>) -> chrono::DateTime<chrono::Utc> {
     388         2648 :         match self {
     389         2636 :             Self::At(t) => t,
     390           12 :             Self::Ago(d) => now
     391           12 :                 .checked_sub_signed(d)
     392           12 :                 .unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC),
     393              :         }
     394         2648 :     }
     395              : 
     396              :     /// The bound at `now` in milliseconds, the precision 4.11 accepts: a
     397              :     /// start (`up`) rounds up and an end down, so rounding only narrows.
     398         2648 :     fn render(self, now: chrono::DateTime<chrono::Utc>, up: bool) -> String {
     399         2648 :         let t = self.at(now);
     400         2648 :         let sub = i64::from(t.timestamp_subsec_nanos() % 1_000_000);
     401         2648 :         let t = if up && sub > 0 {
     402           20 :             t.checked_add_signed(chrono::Duration::nanoseconds(
     403           20 :                 1_000_000_i64.saturating_sub(sub),
     404              :             ))
     405           20 :             .unwrap_or(t)
     406              :         } else {
     407         2628 :             t
     408              :         };
     409         2648 :         t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
     410         2648 :     }
     411              : }
     412              : 
     413              : impl Window {
     414              :     /// Whether an instance observed at `observed_at` falls inside, the
     415              :     /// bounds resolved against `now`; 4.11's half-open comparison.
     416              :     #[must_use]
     417         2436 :     pub fn admits(&self, observed_at: &str, now: chrono::DateTime<chrono::Utc>) -> bool {
     418         2436 :         let t = antares_model::dt_key(observed_at);
     419         2436 :         self.from
     420         2436 :             .is_none_or(|b| t >= antares_model::dt_key(&b.render(now, true)))
     421         2374 :             && self
     422         2374 :                 .to
     423         2374 :                 .is_none_or(|b| t < antares_model::dt_key(&b.render(now, false)))
     424         2436 :     }
     425              : 
     426              :     /// Whether every instance observed at or after `start` is past this
     427              :     /// window's start, the bound resolved against `now`.
     428              :     #[must_use]
     429           82 :     pub fn opens_by(&self, start: &str, now: chrono::DateTime<chrono::Utc>) -> bool {
     430           82 :         self.from.is_none_or(|b| {
     431           82 :             antares_model::dt_key(start) >= antares_model::dt_key(&b.render(now, true))
     432           82 :         })
     433           82 :     }
     434              : }
     435              : 
     436              : /// The text of a [`Filter::warning`]: printable ASCII on one line, at most
     437              : /// [`Warning::MAX_LEN`] bytes, no `"` or `\`, so it travels as one quoted
     438              : /// `warn-text` of an `NGSILD-Warning` value. Composed filters keep each
     439              : /// side's text, at most [`Warning::MAX_TEXTS`].
     440              : #[derive(Debug, Clone, PartialEq, Eq)]
     441              : pub struct Warning(Box<[String]>); // a slice: every Decision carries a Filter's size
     442              : 
     443              : impl Warning {
     444              :     /// Bytes in one text.
     445              :     pub const MAX_LEN: usize = 256;
     446              :     /// Texts a composed filter keeps.
     447              :     pub const MAX_TEXTS: usize = 4;
     448              : 
     449              :     /// One advisory.
     450              :     ///
     451              :     /// # Errors
     452              :     /// When `text` is empty, longer than [`Warning::MAX_LEN`], or holds a
     453              :     /// character outside printable ASCII (a line break, a control
     454              :     /// character), a `"` or a `\\`: it could not travel as one header line.
     455           84 :     pub fn new(text: &str) -> Result<Self, String> {
     456           84 :         if text.is_empty() || text.len() > Self::MAX_LEN {
     457            8 :             return Err(format!(
     458            8 :                 "a policy warning holds 1 to {} bytes",
     459            8 :                 Self::MAX_LEN
     460            8 :             ));
     461           76 :         }
     462           76 :         if !text
     463           76 :             .bytes()
     464         1352 :             .all(|b| (0x20..=0x7e).contains(&b) && b != b'"' && b != b'\\')
     465              :         {
     466           24 :             return Err(
     467           24 :                 "a policy warning is printable ASCII on one line, without '\"' or '\\\\'".into(),
     468           24 :             );
     469           52 :         }
     470           52 :         Ok(Self(Box::new([text.to_owned()])))
     471           84 :     }
     472              : 
     473              :     /// The texts, in the order the filters were composed.
     474              :     #[must_use]
     475           12 :     pub fn texts(&self) -> &[String] {
     476           12 :         &self.0
     477           12 :     }
     478              : 
     479              :     /// `a` and `b` as one: each text once, `a`'s first.
     480              :     #[must_use]
     481           56 :     pub fn join(a: Option<&Self>, b: Option<Self>) -> Option<Self> {
     482           56 :         match (a, b) {
     483           44 :             (Some(a), Some(b)) => {
     484           44 :                 let mut texts = a.0.to_vec();
     485           44 :                 for t in b.0 {
     486           44 :                     if !texts.contains(&t) {
     487           40 :                         texts.push(t);
     488           40 :                     }
     489              :                 }
     490           44 :                 texts.truncate(Self::MAX_TEXTS);
     491           44 :                 Some(Self(texts.into()))
     492              :             }
     493            4 :             (Some(a), None) => Some(a.clone()),
     494            8 :             (None, b) => b,
     495              :         }
     496           56 :     }
     497              : }
     498              : 
     499              : /// One allowlist of a [`Filter`]: the Entities of
     500              : /// `types` for which the condition holds show `attributes`. The condition
     501              : /// (`q`, `scope_q`, `geo_q`, each optional) is the policy's: it is read on
     502              : /// the stored Entity, which may carry Attributes the caller cannot see.
     503              : #[derive(Debug, Clone, Default, PartialEq)]
     504              : pub struct Rule {
     505              :     /// Type names, at least one; a rule naming none holds for nothing.
     506              :     pub types: Vec<String>,
     507              :     /// On the Entity's own members. A linked path (4.9
     508              :     /// `LinkedEntityRelation`) never holds: the visible set of one Entity
     509              :     /// must not depend on another Entity the caller may not read.
     510              :     pub q: Option<Box<QNode>>,
     511              :     pub scope_q: Option<String>,
     512              :     pub geo_q: Option<Box<GeoQuery>>,
     513              :     /// Further areas the Entity must lie in, as [`Filter::geo_also`].
     514              :     pub geo_also: Vec<GeoQuery>,
     515              :     /// Entity ids the rule holds for. With `id_patterns`, an
     516              :     /// Entity is selected when its id is one of these or matches one of
     517              :     /// them; both empty select every id.
     518              :     pub ids: Vec<String>,
     519              :     /// Regular expressions an Entity id may match instead, each anchored
     520              :     /// at both ends. One that does not compile, or compiles past
     521              :     /// `antares_ql::regex`'s bound, matches nothing (fail closed).
     522              :     pub id_patterns: Vec<String>,
     523              :     /// The Attributes an Entity this rule holds for shows.
     524              :     pub attributes: Vec<String>,
     525              :     /// The rule shows every Attribute, and `attributes` is not read
     526              :     /// (amended): an engine's grant over the
     527              :     /// whole Entity. A view never sets it.
     528              :     pub every_attribute: bool,
     529              :     /// Windows of `observedAt`: in a temporal read (5.7.3, 5.7.4)
     530              :     /// an instance of these `attributes` is shown through this rule only if
     531              :     /// it falls into every one. A current-state read shows what the rule
     532              :     /// shows, as the gateway's grant windows did.
     533              :     pub temporal: Vec<Window>,
     534              : }
     535              : 
     536              : impl Rule {
     537              :     /// Every area the Entity must lie in: `geo_q` and `geo_also`.
     538        11700 :     pub fn areas(&self) -> impl Iterator<Item = &GeoQuery> {
     539        11700 :         self.geo_q.as_deref().into_iter().chain(&self.geo_also)
     540        11700 :     }
     541              : 
     542              :     /// Whether the rule names `doc` by one of its types and, when it
     543              :     /// narrows ids, by its id. Neither changes over an Entity's history,
     544              :     /// so a temporal instance is selected the same way (unlike the
     545              :     /// condition, [`Rule::is_conditional`]).
     546        12642 :     pub fn selects(&self, doc: &Value) -> bool {
     547              :         // `entity_types` read in place: this runs per rule per Entity of a
     548              :         // page, and the list it would allocate is read once.
     549        13346 :         let named = |t: &str| self.types.iter().any(|r| r == t);
     550        12642 :         let typed = match doc.get("type") {
     551           48 :             Some(Value::String(t)) => named(t),
     552        12594 :             Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).any(named),
     553            0 :             _ => false,
     554              :         };
     555        12642 :         typed
     556         8334 :             && match doc.get("id").and_then(Value::as_str) {
     557         8326 :                 Some(id) => self.selects_id(id),
     558            8 :                 None => self.ids.is_empty() && self.id_patterns.is_empty(),
     559              :             }
     560        12642 :     }
     561              : 
     562              :     /// Whether the rule's `ids` and `id_patterns` select `id`.
     563         8326 :     pub fn selects_id(&self, id: &str) -> bool {
     564         8326 :         (self.ids.is_empty() && self.id_patterns.is_empty())
     565          442 :             || self.ids.iter().any(|i| i == id)
     566          396 :             || self.id_patterns.iter().any(|p| {
     567          362 :                 antares_ql::regex::compile(&format!("^(?:{p})$")).is_ok_and(|re| re.is_match(id))
     568          362 :             })
     569         8326 :     }
     570              : 
     571              :     /// Whether the rule has a condition beside its types.
     572         3924 :     pub fn is_conditional(&self) -> bool {
     573         3924 :         self.q.is_some() || self.scope_q.is_some() || self.areas().next().is_some()
     574         3924 :     }
     575              : 
     576              :     /// Whether the rule holds for exactly the Entities of its types: no
     577              :     /// condition, no ids, no id patterns (what [`Self::holds`] reads beside
     578              :     /// the types).
     579          432 :     pub fn admits_by_type_alone(&self) -> bool {
     580          432 :         !self.is_conditional() && self.ids.is_empty() && self.id_patterns.is_empty()
     581          432 :     }
     582              : 
     583              :     /// Whether the rule holds for `doc`, an Entity in the form `ctx`
     584              :     /// expands names to (the broker's internal form: expanded keys, each
     585              :     /// Attribute an array of instances).
     586        12492 :     pub fn holds(&self, doc: &Value, ctx: &antares_jsonld::Context) -> bool {
     587        12492 :         self.selects(doc)
     588         8160 :             && self
     589         8160 :                 .scope_q
     590         8160 :                 .as_deref()
     591         8160 :                 .is_none_or(|s| antares_ql::scope::scope_matches(s, doc))
     592         8160 :             && self.areas().all(|g| g.matches(doc, ctx))
     593         7952 :             && self.q.as_deref().is_none_or(|q| {
     594         2234 :                 !q.has_linked_paths() && antares_ql::eval::eval_q(q, doc, ctx, &|_| None)
     595         2234 :             })
     596        12492 :     }
     597              : 
     598              :     /// The Attributes the condition reads, expanded through `ctx`: the head
     599              :     /// of every `q` term and the geoquery's GeoProperty.
     600           82 :     pub fn reads(&self, ctx: &antares_jsonld::Context) -> Vec<String> {
     601           82 :         let mut out: Vec<String> = self
     602           82 :             .q
     603           82 :             .as_deref()
     604           82 :             .map(|q| {
     605           14 :                 q.attribute_paths()
     606           14 :                     .into_iter()
     607           14 :                     .filter_map(|p| p.split(['.', '[', '{']).next())
     608           14 :                     .map(|h| ctx.expand_key(h))
     609           14 :                     .collect()
     610           14 :             })
     611           82 :             .unwrap_or_default();
     612           82 :         for g in self.areas() {
     613           14 :             out.push(if g.geoproperty.is_empty() {
     614           14 :                 antares_ql::geo::LOCATION_IRI.to_owned()
     615              :             } else {
     616            0 :                 ctx.expand_key(&g.geoproperty)
     617              :             });
     618              :         }
     619           82 :         out.sort();
     620           82 :         out.dedup();
     621           82 :         out
     622           82 :     }
     623              : 
     624              :     /// the narrowing rules disclosure: the Attributes the condition reads that
     625              :     /// the rule does not show. Seeing the rule's Attributes tells the
     626              :     /// caller the condition held, so these are what the visible set
     627              :     /// discloses; the view loader reports them.
     628           12 :     pub fn discloses(&self, ctx: &antares_jsonld::Context) -> Vec<String> {
     629           12 :         if self.every_attribute {
     630            4 :             return Vec::new();
     631            8 :         }
     632            8 :         self.reads(ctx)
     633            8 :             .into_iter()
     634           16 :             .filter(|r| !self.attributes.iter().any(|a| ctx.expand_key(a) == *r))
     635            8 :             .collect()
     636           12 :     }
     637              : }
     638              : 
     639              : /// The answer of a broker without an engine: a [`Filter`] that narrows
     640              : /// nothing, one shared instance so asking costs no allocation.
     641        50250 : pub fn unnarrowed() -> std::sync::Arc<Filter> {
     642              :     static NOTHING: LazyLock<std::sync::Arc<Filter>> =
     643           46 :         LazyLock::new(|| std::sync::Arc::new(Filter::default()));
     644        50250 :     NOTHING.clone()
     645        50250 : }
     646              : 
     647              : /// What a [`Filter`]'s rules let one Entity show: `only` (when set) is the
     648              : /// union of the matched rules' Attributes, `omitted` what its types omit.
     649              : #[derive(Debug, Clone, Default, PartialEq)]
     650              : ///
     651              : /// The names borrow from the rules that decided them, so judging an Entity
     652              : /// copies no Attribute name; [`Shown::into_owned`] and
     653              : /// [`Shown::map_names`] give a set that outlives the rules.
     654              : pub struct Shown<'a> {
     655              :     pub only: Option<std::collections::BTreeSet<Cow<'a, str>>>,
     656              :     pub omitted: std::collections::BTreeSet<Cow<'a, str>>,
     657              : }
     658              : 
     659              : impl Shown<'_> {
     660              :     /// Nothing beside the frame: what a projection falls back to for an
     661              :     /// Entity no rule holds for, should one reach it.
     662            4 :     pub fn nothing() -> Self {
     663            4 :         Shown {
     664            4 :             only: Some(Default::default()),
     665            4 :             omitted: Default::default(),
     666            4 :         }
     667            4 :     }
     668              : 
     669        36580 :     pub fn allows(&self, k: &str) -> bool {
     670        36580 :         self.only.as_ref().is_none_or(|o| o.contains(k)) && !self.omitted.contains(k)
     671        36580 :     }
     672              : 
     673              :     /// The same set with every name mapped, for a document in another form.
     674            8 :     pub fn map_names(&self, f: impl Fn(&str) -> String) -> Shown<'static> {
     675              :         Shown {
     676            8 :             only: self
     677            8 :                 .only
     678            8 :                 .as_ref()
     679            8 :                 .map(|o| o.iter().map(|n| Cow::Owned(f(n))).collect()),
     680            8 :             omitted: self.omitted.iter().map(|n| Cow::Owned(f(n))).collect(),
     681              :         }
     682            8 :     }
     683              : 
     684              :     /// The same set, owning its names.
     685            4 :     pub fn into_owned(self) -> Shown<'static> {
     686            8 :         let own = |n: Cow<'_, str>| Cow::Owned(n.into_owned());
     687              :         Shown {
     688            4 :             only: self.only.map(|o| o.into_iter().map(own).collect()),
     689            4 :             omitted: self.omitted.into_iter().map(own).collect(),
     690              :         }
     691            4 :     }
     692              : }
     693              : 
     694              : /// The types of an Entity document: its `type` member, a string or an
     695              : /// array of strings (5.2.4), in whatever form the document is in.
     696          814 : pub fn entity_types(doc: &Value) -> Vec<&str> {
     697          814 :     match doc.get("type") {
     698            8 :         Some(Value::String(t)) => vec![t.as_str()],
     699          708 :         Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).collect(),
     700           98 :         _ => Vec::new(),
     701              :     }
     702          814 : }
     703              : 
     704              : /// What identifies the document rather than describing it: 5.2.4 makes
     705              : /// `id` and `type` mandatory members of an Entity, and an answer without
     706              : /// them is not an Entity at all. A projection never removes these.
     707              : const FRAME: [&str; 3] = ["id", "type", "@context"];
     708              : 
     709              : impl Filter {
     710              :     /// Every area the Entity must lie in: `geo_q` and `geo_also`.
     711        28213 :     pub fn areas(&self) -> impl Iterator<Item = &GeoQuery> {
     712        28213 :         self.geo_q.as_deref().into_iter().chain(&self.geo_also)
     713        28213 :     }
     714              : 
     715              :     /// Apply `pick`/`omit` and the rules to one document by member name.
     716              :     /// This is the reference semantics `run_policy_contract` holds an
     717              :     /// engine's answer against; a served document is projected through the
     718              :     /// request's own 5.5.2 representation instead
     719              :     /// (`repr::narrow_projection`), where the names are expanded against
     720              :     /// the request `@context` first. Rule conditions are read with the core
     721              :     /// `@context`; a document no rule holds for keeps its frame here, while
     722              :     /// the served paths leave it out.
     723           42 :     pub fn project(&self, doc: &mut Value) {
     724           42 :         let shown = self.has_rules().then(|| {
     725            4 :             self.shown(doc, &CORE_CONTEXT)
     726            4 :                 .unwrap_or_else(Shown::nothing)
     727            4 :         });
     728           42 :         let Some(obj) = doc.as_object_mut() else {
     729            4 :             return;
     730              :         };
     731           38 :         if !self.pick.is_empty() {
     732           36 :             obj.retain(|k, _| FRAME.contains(&k.as_str()) || self.pick.iter().any(|p| p == k));
     733           28 :         }
     734           38 :         for name in &self.omit {
     735           28 :             if FRAME.contains(&name.as_str()) {
     736            8 :                 continue;
     737           20 :             }
     738           20 :             obj.remove(name);
     739              :         }
     740           38 :         if let Some(shown) = shown {
     741           28 :             obj.retain(|k, _| FRAME.contains(&k.as_str()) || shown.allows(k));
     742           34 :         }
     743           42 :     }
     744              : 
     745              :     /// True when the filter would change nothing, which is how an engine
     746              :     /// that means "allow" can say so with an empty `Filter`.
     747          862 :     pub fn is_empty(&self) -> bool {
     748          862 :         self.q.is_none()
     749          830 :             && self.scope_q.is_none()
     750          830 :             && self.areas().next().is_none()
     751          826 :             && self.omit.is_empty()
     752          750 :             && self.pick.is_empty()
     753          750 :             && !self.has_rules()
     754          682 :             && self.temporal.is_empty()
     755          862 :     }
     756              : 
     757              :     /// Whether the filter removes anything from what it selects: a
     758              :     /// projection, a per-type omit, a rule's Attribute list or a temporal
     759              :     /// window. A write condition ([`Decision::AllowIf`]) may not: the write
     760              :     /// is performed whole or not at all.
     761          388 :     pub fn projects(&self) -> bool {
     762          388 :         !self.pick.is_empty()
     763          376 :             || !self.omit.is_empty()
     764          350 :             || !self.omit_by_type.is_empty()
     765          346 :             || !self.temporal.is_empty()
     766          342 :             || self
     767          342 :                 .rules
     768          342 :                 .iter()
     769          342 :                 .any(|r| !r.attributes.is_empty() || !r.temporal.is_empty())
     770          388 :     }
     771              : 
     772              :     /// Whether the filter carries rules or per-type omits.
     773        28319 :     pub fn has_rules(&self) -> bool {
     774        28319 :         !self.rules.is_empty() || !self.omit_by_type.is_empty()
     775        28319 :     }
     776              : 
     777              :     /// Whether a rule holds for `doc` (every document without rules): what
     778              :     /// [`Self::shown`] answers `Some` for, without building the set.
     779         1716 :     pub fn admits(&self, doc: &Value, ctx: &antares_jsonld::Context) -> bool {
     780         2576 :         self.rules.is_empty() || self.rules.iter().any(|r| r.holds(doc, ctx))
     781         1716 :     }
     782              : 
     783              :     /// what `doc` may show, or `None` when `rules` is
     784              :     /// non-empty and none holds for it (the Entity is out). An Entity sees
     785              :     /// the union of the Attributes of the rules that hold for it, less what
     786              :     /// any of its types omits. Names compare as written: the caller brings
     787              :     /// rules and document into one form first (`repr::type_rules` expands
     788              :     /// both), and `ctx` is the one the conditions' names expand through.
     789         5680 :     pub fn shown(&self, doc: &Value, ctx: &antares_jsonld::Context) -> Option<Shown<'_>> {
     790         5680 :         let only = if self.rules.is_empty() {
     791           18 :             None
     792              :         } else {
     793         9670 :             let mut held = self.rules.iter().filter(|r| r.holds(doc, ctx)).peekable();
     794         5662 :             held.peek()?;
     795         4788 :             let held: Vec<&Rule> = held.collect();
     796         4788 :             if held.iter().any(|r| r.every_attribute) {
     797            2 :                 None
     798              :             } else {
     799              :                 Some(
     800         4786 :                     held.iter()
     801        10056 :                         .flat_map(|r| r.attributes.iter().map(|a| Cow::Borrowed(a.as_str())))
     802         4786 :                         .collect(),
     803              :                 )
     804              :             }
     805              :         };
     806         4806 :         if self.omit_by_type.is_empty() {
     807         4784 :             return Some(Shown {
     808         4784 :                 only,
     809         4784 :                 omitted: Default::default(),
     810         4784 :             });
     811           22 :         }
     812           22 :         let omitted = entity_types(doc)
     813           22 :             .into_iter()
     814           32 :             .filter_map(|t| self.omit_by_type.get(t))
     815           22 :             .flatten()
     816           22 :             .map(|a| Cow::Borrowed(a.as_str()))
     817           22 :             .collect();
     818           22 :         Some(Shown { only, omitted })
     819         5680 :     }
     820              : 
     821              :     /// the narrowing rules per rule (K22): a write is allowed only when some
     822              :     /// rule holds for its post-image and every Attribute the write names is
     823              :     /// shown by a rule that holds, none of which its types omit: the write
     824              :     /// sets what the Entity then shows, the union of its rules. One rule
     825              :     /// need not show them all, so a type whose list a view splits over
     826              :     /// several rules is written whole. The check a view's engine
     827              :     /// runs before it allows a write.
     828           48 :     pub fn admits_write(
     829           48 :         &self,
     830           48 :         post_image: &Value,
     831           48 :         written: &[&str],
     832           48 :         ctx: &antares_jsonld::Context,
     833           48 :     ) -> bool {
     834           48 :         let types = entity_types(post_image);
     835           64 :         let omitted = |w: &str| {
     836           64 :             types
     837           64 :                 .iter()
     838           64 :                 .filter_map(|t| self.omit_by_type.get(*t))
     839           64 :                 .any(|o| o.iter().any(|a| a == w))
     840           64 :         };
     841           64 :         if written.iter().any(|w| omitted(w)) {
     842            4 :             return false;
     843           44 :         }
     844           44 :         if self.rules.is_empty() {
     845            4 :             return true;
     846           40 :         }
     847           40 :         let holding: Vec<&Rule> = self
     848           40 :             .rules
     849           40 :             .iter()
     850           92 :             .filter(|r| r.holds(post_image, ctx))
     851           40 :             .collect();
     852           40 :         !holding.is_empty()
     853           32 :             && (holding.iter().any(|r| r.every_attribute)
     854           28 :                 || written
     855           28 :                     .iter()
     856           68 :                     .all(|w| holding.iter().any(|r| r.attributes.iter().any(|a| a == w))))
     857           48 :     }
     858              : 
     859              :     /// Say the answer was narrowed, and add the engine's warning, when the
     860              :     /// engine asked for either. Narrowing is otherwise silent: a caller
     861              :     /// cannot tell an Entity it may not see from one that is not there.
     862              :     /// `alias` is the warn-agent, this broker's host alias.
     863         6358 :     pub fn mark(&self, alias: &str, headers: &mut HeaderMap) {
     864         6358 :         if self.restricted {
     865           30 :             headers.insert(
     866           30 :                 RESTRICTED_HEADER,
     867           30 :                 axum::http::HeaderValue::from_static("true"),
     868           30 :             );
     869         6328 :         }
     870         6358 :         for text in self.warning.iter().flat_map(Warning::texts) {
     871              :             // 199 Miscellaneous warning (RFC 7234 5.5), as the gateway's
     872              :             // dropped-types warning was.
     873            4 :             let value = crate::federation::warning(199, alias, text);
     874            4 :             if let Ok(v) = axum::http::HeaderValue::from_str(&value) {
     875            4 :                 headers.append("NGSILD-Warning", v);
     876            4 :             }
     877              :         }
     878         6358 :     }
     879              : 
     880              :     /// The request's temporal parameters with its 4.11 window clamped to
     881              :     /// [`Filter::temporal`], resolved against now, for every reader
     882              :     /// after the gate: the store's instance range, the forward, the links.
     883              :     /// `required` is the operation's (5.7.4 demands a window, 5.7.3 does
     884              :     /// not), so a request that lacked one is still refused. The windows are
     885              :     /// on `observedAt`: a request on another timeproperty is answered
     886              :     /// nothing, since the store applies one range on one timeproperty.
     887              :     ///
     888              :     /// # Errors
     889              :     /// The request's own 4.11 window is invalid.
     890         2244 :     pub fn narrow_window(
     891         2244 :         &self,
     892         2244 :         params: &std::collections::HashMap<String, String>,
     893         2244 :         required: bool,
     894         2244 :     ) -> Result<std::collections::HashMap<String, String>, antares_model::NgsiError> {
     895         2244 :         let mut out = params.clone();
     896         2244 :         if self.temporal.is_empty() {
     897         2180 :             return Ok(out);
     898           64 :         }
     899           64 :         let asked = crate::temporalq::TemporalQ::from_params(params, required)?;
     900          148 :         let key = |s: &String| antares_model::dt_key(s);
     901           56 :         let (mut from, mut to) = match &asked {
     902           34 :             Some(tq) if tq.timeproperty != "observedAt" => {
     903            6 :                 let epoch = "1970-01-01T00:00:00Z".to_owned();
     904            6 :                 (Some(epoch.clone()), Some(epoch))
     905              :             }
     906           28 :             Some(tq) => match tq.timerel.as_str() {
     907           28 :                 "before" => (None, Some(tq.time_at.clone())),
     908           18 :                 "after" => (Some(tq.time_at.clone()), None),
     909            4 :                 "between" => (Some(tq.time_at.clone()), tq.end_time_at.clone()),
     910            0 :                 _ => (None, None),
     911              :             },
     912           22 :             None => (None, None),
     913              :         };
     914           56 :         let now = chrono::Utc::now();
     915           66 :         for w in &self.temporal {
     916           66 :             if let Some(b) = w.from.map(|b| b.render(now, true)) {
     917           44 :                 from = Some(match from {
     918           20 :                     Some(f) if key(&f) >= key(&b) => f,
     919           40 :                     _ => b,
     920              :                 });
     921           22 :             }
     922           66 :             if let Some(b) = w.to.map(|b| b.render(now, false)) {
     923           38 :                 to = Some(match to {
     924           18 :                     Some(t) if key(&t) <= key(&b) => t,
     925           24 :                     _ => b,
     926              :                 });
     927           28 :             }
     928              :         }
     929              :         // `between` an instant and itself admits nothing: the answer is
     930              :         // empty, as for a window over a period with no data
     931           56 :         if let (Some(f), Some(t)) = (&from, &to) {
     932           36 :             if key(f) >= key(t) {
     933           18 :                 to = Some(f.clone());
     934           18 :             }
     935           20 :         }
     936           56 :         let (timerel, time_at, end) = match (from, to) {
     937           36 :             (Some(f), Some(t)) => ("between", f, Some(t)),
     938           12 :             (Some(f), None) => ("after", f, None),
     939            4 :             (None, Some(t)) => ("before", t, None),
     940            4 :             (None, None) => return Ok(out),
     941              :         };
     942           52 :         out.insert("timerel".into(), timerel.into());
     943           52 :         out.insert("timeAt".into(), time_at);
     944           52 :         match end {
     945           36 :             Some(e) => out.insert("endTimeAt".into(), e),
     946           16 :             None => out.remove("endTimeAt"),
     947              :         };
     948           52 :         Ok(out)
     949         2244 :     }
     950              : 
     951              :     /// The request's own query parameters, narrowed by this decision.
     952              :     ///
     953              :     /// The `q` conjunction is made on the AST and rendered back once
     954              :     /// (`antares_ql`'s renderer parenthesises an `Or` inside an `And`, so
     955              :     /// 4.9's `;`-over-`|` precedence is the renderer's problem and not the
     956              :     /// rule writer's). Every consumer below reads the narrowed parameters:
     957              :     /// the store push-down, the local re-check that 5.7.2.4 runs over
     958              :     /// merged results, and the query the request is forwarded with.
     959              :     ///
     960              :     /// A `scopeQ` narrowing is set when the request carries none, and
     961              :     /// intersected with the request's own when it carries one. 4.19's `and`
     962              :     /// is over independent per-pattern predicates, so it distributes over
     963              :     /// the `,`/`|` disjunction and the intersection is itself a Scope
     964              :     /// Query — `antares_ql::scope::intersect_scope_q` writes it. Where the
     965              :     /// product cannot be written down the seam answers the only way that is
     966              :     /// not wider than the engine decided: it refuses.
     967              :     ///
     968              :     /// `geo_q` stays out of the parameters: they hold one geoquery (7.2.5)
     969              :     /// and it is the request's. The narrowing's area reaches the store
     970              :     /// query beside it through `repr::narrowing`.
     971              :     ///
     972              :     /// a term of the request's own `q` on an Attribute
     973              :     /// this `pick`/`omit` hides reads as a term on an Attribute the Entity
     974              :     /// does not have (see [`Filter::hide_terms`]). `ctx` is the request's,
     975              :     /// the one its `q` terms expand through.
     976         3786 :     pub fn narrow_params(
     977         3786 :         &self,
     978         3786 :         params: &std::collections::HashMap<String, String>,
     979         3786 :         ctx: &antares_jsonld::Context,
     980         3786 :     ) -> Result<std::collections::HashMap<String, String>, Denied> {
     981         3786 :         let mut out = params.clone();
     982              :         // the request's own `q` is parsed and refused long before a filter
     983              :         // reaches it; an unparsable one here is a caller that narrowed
     984              :         // something it never validated
     985         3786 :         let own = match out.get("q").map(|q| antares_ql::parse_q(q)).transpose() {
     986         3786 :             Ok(own) => own.map(|own| self.hide_terms(own, ctx)),
     987            0 :             Err(_) => return Err(Denied(ENGINE_FAILED.to_owned())),
     988              :         };
     989              :         // threat V23: the policy part is held to its share of the narrowed
     990              :         // caps on its own, so whether it fits never depends on the caller's
     991              :         // `q` (see `antares_ql::parse_narrowed_q`)
     992         3786 :         if self
     993         3786 :             .q
     994         3786 :             .as_deref()
     995         3786 :             .is_some_and(|q| !antares_ql::fits_policy_q(q))
     996              :         {
     997            6 :             return Err(Denied(ENGINE_FAILED.to_owned()));
     998         3780 :         }
     999         3780 :         let narrowed = match (own, &self.q) {
    1000           34 :             (Some(own), Some(extra)) => Some(QNode::And(vec![own, (**extra).clone()])),
    1001         3746 :             (own, extra) => own.or_else(|| extra.as_deref().cloned()),
    1002              :         };
    1003         3780 :         if let Some(narrowed) = narrowed {
    1004          776 :             out.insert("q".to_owned(), narrowed.to_string());
    1005         3004 :         }
    1006         3780 :         if let Some(scope) = &self.scope_q {
    1007           22 :             let narrowed = match out.get("scopeQ") {
    1008           14 :                 None => scope.clone(),
    1009            8 :                 Some(own) => antares_ql::scope::intersect_scope_q(own, scope)
    1010            8 :                     .ok_or_else(|| Denied(SCOPE_NOT_NARROWABLE.to_owned()))?,
    1011              :             };
    1012           20 :             out.insert("scopeQ".to_owned(), narrowed);
    1013         3758 :         }
    1014         3778 :         Ok(out)
    1015         3786 :     }
    1016              : }
    1017              : 
    1018              : impl Filter {
    1019              :     /// Whether this `pick`/`omit` hides the Attribute a q term names as
    1020              :     /// `written`, `iri` once expanded. A pick narrows Attributes only: the
    1021              :     /// system members it never names stay readable, as in the projection.
    1022         5768 :     fn hides(&self, written: &str, iri: &str) -> bool {
    1023         5780 :         let named = |names: &[String]| {
    1024         5780 :             names
    1025         5780 :                 .iter()
    1026         5780 :                 .any(|n| crate::repr::policy_context().expand_key(n) == iri)
    1027         5780 :         };
    1028         5768 :         named(&self.omit)
    1029         3096 :             || (!self.pick.is_empty()
    1030           12 :                 && antares_ql::is_non_system_attr(written)
    1031           12 :                 && !named(&self.pick))
    1032         5768 :     }
    1033              : 
    1034              :     /// the narrowing rules: a term on an Attribute this narrowing
    1035              :     /// hides reads as a term on one the Entity does not have — a
    1036              :     /// comparison or `attr` is false, `!attr` is true — so the hidden value
    1037              :     /// is never compared, on the store path or in memory. The q grammar
    1038              :     /// has no constants, so false is written `a;!a` and true `a|!a`, on
    1039              :     /// the hidden name itself: both hold whatever the Entity carries. The
    1040              :     /// path's top Attribute decides, so a sub-attribute or a hop through a
    1041              :     /// hidden Relationship is as hidden as the Attribute. Per-type rules
    1042              :     /// are not read here: their q is read on what the rules show
    1043              :     /// (`repr::client_q`).
    1044         6484 :     pub fn hide_terms(&self, node: QNode, ctx: &antares_jsonld::Context) -> QNode {
    1045         6484 :         if self.omit.is_empty() && self.pick.is_empty() {
    1046          704 :             return node;
    1047         5780 :         }
    1048         5780 :         let constant = |top: &str, holds: bool| {
    1049         2670 :             let is = |negated| QNode::Exists {
    1050         5340 :                 path: antares_ql::QPath::dotted(vec![top.to_owned()]),
    1051         5340 :                 negated,
    1052         5340 :             };
    1053         2670 :             let pair = vec![is(false), is(true)];
    1054         2670 :             if holds {
    1055            6 :                 QNode::Or(pair)
    1056              :             } else {
    1057         2664 :                 QNode::And(pair)
    1058              :             }
    1059         2670 :         };
    1060         5780 :         let hidden = |path: &antares_ql::QPath| {
    1061         5752 :             path.top()
    1062         5752 :                 .filter(|top| self.hides(top, &ctx.expand_key(top)))
    1063         5752 :                 .map(str::to_owned)
    1064         5752 :         };
    1065         5780 :         match node {
    1066            2 :             QNode::And(nodes) => {
    1067            4 :                 QNode::And(nodes.into_iter().map(|n| self.hide_terms(n, ctx)).collect())
    1068              :             }
    1069           26 :             QNode::Or(nodes) => {
    1070         5726 :                 QNode::Or(nodes.into_iter().map(|n| self.hide_terms(n, ctx)).collect())
    1071              :             }
    1072         5744 :             QNode::Cmp { path, op, value } => match hidden(&path) {
    1073         2662 :                 Some(top) => constant(&top, false),
    1074         3082 :                 None => QNode::Cmp { path, op, value },
    1075              :             },
    1076            8 :             QNode::Exists { path, negated } => match hidden(&path) {
    1077            8 :                 Some(top) => constant(&top, negated),
    1078            0 :                 None => QNode::Exists { path, negated },
    1079              :             },
    1080              :         }
    1081         6484 :     }
    1082              : }
    1083              : 
    1084              : /// The response header a `Filter { restricted: true }` adds. It is not in
    1085              : /// the `NGSILD-` namespace: that prefix is ETSI's, carries the headers
    1086              : /// clause 6.3 defines (`NGSILD-Tenant`, `NGSILD-EntityMap`,
    1087              : /// `NGSILD-Results-Count`, `NGSILD-Warning`), and a broker-invented header
    1088              : /// under it would collide with whatever a later version puts there — the
    1089              : /// same reason a refusal answers `urn:antares:error:AccessDenied` rather
    1090              : /// than an invented `uri.etsi.org` type.
    1091              : pub const RESTRICTED_HEADER: &str = "Antares-Results-Restricted";
    1092              : 
    1093              : /// The refusal a `scopeQ` narrowing gets when its intersection with the
    1094              : /// request's own cannot be written as a Scope Query — one side selects
    1095              : /// nothing, or the distributed product is too large to express (see
    1096              : /// [`Filter::narrow_params`]).
    1097              : pub const SCOPE_NOT_NARROWABLE: &str =
    1098              :     "the request's own scope query cannot be narrowed by the policy engine";
    1099              : 
    1100              : /// What an engine may answer about one notification, for one subscription.
    1101              : #[derive(Debug, Clone, PartialEq)]
    1102              : pub enum NotifyDecision {
    1103              :     Deliver,
    1104              :     /// Send it, narrowed: the same `pick`/`omit` projection the query path
    1105              :     /// applies, over the entities of `data`. A notification `Filter` that
    1106              :     /// carries `q` or `scopeQ` is refused as a [`NotifyDecision::Drop`]:
    1107              :     /// the entities were selected by the subscription's own conditions long
    1108              :     /// before this point, there is nothing left to re-run the query against,
    1109              :     /// and delivering the notification unfiltered would tell the engine a
    1110              :     /// narrowing was applied that never was.
    1111              :     Filter(Filter),
    1112              :     /// Do not send. 5.8.6 counts this as no attempt at all — the
    1113              :     /// notification was never sent, so it is neither a success nor a
    1114              :     /// failure, and `timesSent` does not move.
    1115              :     Drop,
    1116              : }
    1117              : 
    1118              : /// The seam. One implementation ships with the broker; every other one is
    1119              : /// an addon crate a deployment builds itself.
    1120              : pub trait PolicyEngine: Send + Sync {
    1121              :     /// The name a deployment selects the engine by, and the name
    1122              :     /// `/q/health` reports.
    1123              :     fn name(&self) -> &str;
    1124              : 
    1125              :     /// Fires once per request, after negotiation and expansion, before the
    1126              :     /// operation and before any fan-out (ADR-0014 `on_request`).
    1127              :     fn decide<'a>(&'a self, subject: &'a Subject, op: &'a Operation<'a>) -> DecisionFuture<'a>;
    1128              : 
    1129              :     /// Fires once per batch (5.6.7 to 5.6.10, 5.6.20) with one Operation
    1130              :     /// per member, so a batch is refused member by member and the allowed
    1131              :     /// members are applied (207). The answer holds one Decision per
    1132              :     /// Operation, in order; an answer of any other length denies the whole
    1133              :     /// batch. The default asks [`decide`](Self::decide) once per member; an
    1134              :     /// engine that asks a server overrides it to ask once.
    1135           74 :     fn decide_each<'a>(
    1136           74 :         &'a self,
    1137           74 :         subject: &'a Subject,
    1138           74 :         ops: &'a [Operation<'a>],
    1139           74 :     ) -> DecisionsFuture<'a> {
    1140           74 :         Box::pin(async move {
    1141           74 :             let mut out = Vec::with_capacity(ops.len());
    1142          280 :             for op in ops {
    1143          280 :                 out.push(self.decide(subject, op).await);
    1144              :             }
    1145           74 :             out
    1146           74 :         })
    1147           74 :     }
    1148              : 
    1149              :     /// The probe a surface lists its operations by (a surface's listing): one
    1150              :     /// Operation per clause, naming nothing, and each answer says whether
    1151              :     /// some request of that clause could be allowed for `subject`, never
    1152              :     /// whether this one is. The default decides the empty Operations as
    1153              :     /// [`decide_each`](Self::decide_each) would; an engine whose rules need
    1154              :     /// a type to answer a read overrides it, so a grant that takes the
    1155              :     /// queried types is offered, and a refusal of every request is not.
    1156            4 :     fn offers_each<'a>(
    1157            4 :         &'a self,
    1158            4 :         subject: &'a Subject,
    1159            4 :         ops: &'a [Operation<'a>],
    1160            4 :     ) -> DecisionsFuture<'a> {
    1161            4 :         self.decide_each(subject, ops)
    1162            4 :     }
    1163              : 
    1164              :     /// Fires once per notification document per subscription, before the
    1165              :     /// egress check and the send (ADR-0014 `pre_notify`). Synchronous: it
    1166              :     /// sits inside the delivery the broker is about to make, and an engine
    1167              :     /// that has to ask a server for every notification is a design the seam
    1168              :     /// declines to make easy.
    1169              :     fn pre_notify(
    1170              :         &self,
    1171              :         subject: &Subject,
    1172              :         sub: &Value,
    1173              :         notification: &mut Value,
    1174              :     ) -> NotifyDecision;
    1175              : 
    1176              :     /// Whether the engine reads rule documents through
    1177              :     /// [`load`](Self::load) (ADR-0020 amendment). No by default: `/q/policy`
    1178              :     /// then answers 404, as it does for a broker without an engine.
    1179            4 :     fn loads_documents(&self) -> bool {
    1180            4 :         false
    1181            4 :     }
    1182              : 
    1183              :     /// Take `document` as the rule document of `tenant`, or drop that
    1184              :     /// Tenant's document when it is `Value::Null`. An `Err` leaves the
    1185              :     /// engine as it was: the previous document keeps deciding. The reason
    1186              :     /// goes back to the admin caller who wrote the document and nowhere
    1187              :     /// else, so it may name parts of it. The default refuses every
    1188              :     /// document.
    1189              :     ///
    1190              :     /// # Errors
    1191              :     /// Why the engine refuses the document.
    1192            4 :     fn load(&self, tenant: &str, document: &Value) -> Result<(), String> {
    1193            4 :         let _ = (tenant, document);
    1194            4 :         Err(format!(
    1195            4 :             "the policy engine {:?} reads no rule documents",
    1196            4 :             self.name()
    1197            4 :         ))
    1198            4 :     }
    1199              : }
    1200              : 
    1201              : /// The name of the engine the broker ships, which is also the name
    1202              : /// `/q/health` reports when no engine is attached at all: the two are the
    1203              : /// same decision — allow — and an operator reading it learns what the
    1204              : /// broker does, not which object made it happen.
    1205              : pub const BUILT_IN_NAME: &str = "allow-all";
    1206              : 
    1207              : /// The engine the broker ships, and the one conformance is asserted
    1208              : /// against: it decides nothing, so the broker behaves exactly as it did
    1209              : /// before the seam existed.
    1210              : ///
    1211              : /// A deployment that attaches no engine does not get an instance of this:
    1212              : /// it gets no engine at all ([`AppState::policy`](crate::state::AppState)
    1213              : /// is `None`), and every gate answers the empty [`Filter`] without building
    1214              : /// a [`Subject`], boxing a future or arming the [`TIMEOUT`] timer. The two
    1215              : /// paths decide the same thing, which is what
    1216              : /// `the_built_in_answer_is_the_bypassed_answer` holds them to.
    1217              : pub struct AllowAll;
    1218              : 
    1219              : impl PolicyEngine for AllowAll {
    1220           12 :     fn name(&self) -> &str {
    1221           12 :         BUILT_IN_NAME
    1222           12 :     }
    1223              : 
    1224          224 :     fn decide<'a>(&'a self, _subject: &'a Subject, _op: &'a Operation<'a>) -> DecisionFuture<'a> {
    1225          224 :         Box::pin(std::future::ready(Decision::Allow))
    1226          224 :     }
    1227              : 
    1228            4 :     fn pre_notify(&self, _s: &Subject, _sub: &Value, _n: &mut Value) -> NotifyDecision {
    1229            4 :         NotifyDecision::Deliver
    1230            4 :     }
    1231              : }
    1232              : 
    1233              : /// A `Filter` only means something where the broker can serve less than it
    1234              : /// was asked for, and [`FILTERABLE`] is that list. Everywhere else the
    1235              : /// narrowing would be silently dropped: "purge every entity, but only the
    1236              : /// ones you may see" is a different operation from the one the client
    1237              : /// asked for and would delete less than the 204 claims, and a create writes
    1238              : /// its Entity or does not. Those operations take the strict reading: narrow
    1239              : /// means refuse. An empty `Filter` asks for nothing and is an allow.
    1240         4140 : pub fn resolve(clause: &str, decision: Decision) -> Decision {
    1241         1004 :     match decision {
    1242          332 :         Decision::AllowIf(_) if !WRITE_BY_ID.contains(&clause) => Decision::Deny(format!(
    1243           30 :             "{clause} is not a write by id and cannot be conditioned on the stored Entity"
    1244           30 :         )),
    1245          302 :         Decision::AllowIf(f) if f.projects() => {
    1246           24 :             Decision::Deny("a write condition selects Entities and projects nothing".into())
    1247              :         }
    1248          278 :         Decision::AllowIf(f) if Condition::of(&f).is_none() => Decision::Allow,
    1249         1032 :         Decision::Filter(f) if RECORD_LISTS.contains(&clause) && f.projects() => Decision::Deny(
    1250           28 :             format!("{clause} lists records whole and a narrowing there cannot project"),
    1251           28 :         ),
    1252         1004 :         Decision::Filter(f) if !FILTERABLE.contains(&clause) && !NAME_READS.contains(&clause) => {
    1253           94 :             if f.is_empty() {
    1254           10 :                 Decision::Allow
    1255           84 :             } else if WHOLE_TENANT.contains(&clause) {
    1256           40 :                 Decision::Deny(format!(
    1257           40 :                     "{clause} acts on everything the tenant holds and cannot be narrowed"
    1258           40 :                 ))
    1259              :             } else {
    1260           44 :                 Decision::Deny(format!(
    1261           44 :                     "{clause} is performed whole or not at all and cannot be narrowed"
    1262           44 :                 ))
    1263              :             }
    1264              :         }
    1265         3898 :         other => other,
    1266              :     }
    1267         4140 : }
    1268              : 
    1269              : /// Ask the engine, and fail closed. An engine that panics, or that runs
    1270              : /// past [`TIMEOUT`], denies: a seam that waved the request through on its
    1271              : /// own failure would turn a broken addon into an open door.
    1272              : ///
    1273              : /// The panic is caught in place rather than on a spawned task, because the
    1274              : /// task boundary would demand `'static` of the operation, which borrows the
    1275              : /// request. It is caught TWICE, because an engine has two places to fail:
    1276              : /// `decide` returns a boxed future, and a synchronous engine does its whole
    1277              : /// decision in the call that builds that future — the reference engine is
    1278              : /// `Box::pin(ready(self.judge(..)))` — so guarding only the future guards
    1279              : /// the half that does nothing.
    1280              : ///
    1281              : /// [`TIMEOUT`] can only race the future. Work done before the future exists
    1282              : /// holds the executor thread, and no timer inside the same task can
    1283              : /// interrupt it; an engine that blocks is a deployment's own bug, and the
    1284              : /// bound that catches it is the request timeout in front of the broker.
    1285         3528 : pub async fn decide(engine: &dyn PolicyEngine, subject: &Subject, op: &Operation<'_>) -> Decision {
    1286              :     #[cfg(not(target_arch = "wasm32"))]
    1287              :     {
    1288              :         use futures_util::FutureExt as _;
    1289         3528 :         let built =
    1290         3528 :             std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| engine.decide(subject, op)));
    1291         3528 :         let Ok(fut) = built else {
    1292            4 :             tracing::error!("policy engine {} panicked; denying", engine.name());
    1293            4 :             metrics::counter!("antares_policy_failures_total", "reason" => "panic").increment(1);
    1294            4 :             return Decision::Deny(ENGINE_FAILED.to_owned());
    1295              :         };
    1296         3524 :         let guarded = std::panic::AssertUnwindSafe(fut).catch_unwind();
    1297         3524 :         match tokio::time::timeout(*TIMEOUT, guarded).await {
    1298         3508 :             Ok(Ok(d)) => resolve(op.clause, d),
    1299              :             Ok(Err(_)) => {
    1300            4 :                 tracing::error!("policy engine {} panicked; denying", engine.name());
    1301            4 :                 metrics::counter!("antares_policy_failures_total", "reason" => "panic")
    1302            4 :                     .increment(1);
    1303            4 :                 Decision::Deny(ENGINE_FAILED.to_owned())
    1304              :             }
    1305              :             Err(_) => {
    1306           12 :                 tracing::error!(
    1307              :                     "policy engine {} did not answer within {:?}; denying",
    1308            0 :                     engine.name(),
    1309            0 :                     *TIMEOUT
    1310              :                 );
    1311           12 :                 metrics::counter!("antares_policy_failures_total", "reason" => "timeout")
    1312           12 :                     .increment(1);
    1313           12 :                 Decision::Deny(ENGINE_TIMED_OUT.to_owned())
    1314              :             }
    1315              :         }
    1316              :     }
    1317              :     // The browser build has no timer to race against and aborts on panic;
    1318              :     // it also loads no addon, so the engine is always the built-in one.
    1319              :     #[cfg(target_arch = "wasm32")]
    1320              :     {
    1321              :         resolve(op.clause, engine.decide(subject, op).await)
    1322              :     }
    1323         3528 : }
    1324              : 
    1325              : /// [`decide`] for a batch: one answer per Operation, failing closed the
    1326              : /// same way. A panic, a timeout (one [`TIMEOUT`] for the whole batch) or an
    1327              : /// answer whose length does not match the batch denies every member, and
    1328              : /// each answer passes [`resolve`] for its own clause.
    1329           84 : pub async fn decide_each(
    1330           84 :     engine: &dyn PolicyEngine,
    1331           84 :     subject: &Subject,
    1332           84 :     ops: &[Operation<'_>],
    1333           84 : ) -> Vec<Decision> {
    1334           84 :     guarded(engine, ops, || engine.decide_each(subject, ops)).await
    1335           84 : }
    1336              : 
    1337              : /// One batch answer of `engine` to `ops` from `ask`, failing closed as
    1338              : /// [`decide_each`] documents, each answer passed through [`resolve`].
    1339           88 : async fn guarded<'a>(
    1340           88 :     engine: &dyn PolicyEngine,
    1341           88 :     ops: &[Operation<'_>],
    1342           88 :     ask: impl FnOnce() -> DecisionsFuture<'a>,
    1343           88 : ) -> Vec<Decision> {
    1344           88 :     let all = |why: &str| vec![Decision::Deny(why.to_owned()); ops.len()];
    1345              :     #[cfg(not(target_arch = "wasm32"))]
    1346           88 :     let answer = {
    1347              :         use futures_util::FutureExt as _;
    1348           88 :         let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(ask));
    1349           88 :         let Ok(fut) = built else {
    1350            0 :             tracing::error!("policy engine {} panicked; denying", engine.name());
    1351            0 :             metrics::counter!("antares_policy_failures_total", "reason" => "panic").increment(1);
    1352            0 :             return all(ENGINE_FAILED);
    1353              :         };
    1354           88 :         let guarded = std::panic::AssertUnwindSafe(fut).catch_unwind();
    1355           88 :         match tokio::time::timeout(*TIMEOUT, guarded).await {
    1356           88 :             Ok(Ok(d)) => d,
    1357              :             Ok(Err(_)) => {
    1358            0 :                 tracing::error!("policy engine {} panicked; denying", engine.name());
    1359            0 :                 metrics::counter!("antares_policy_failures_total", "reason" => "panic")
    1360            0 :                     .increment(1);
    1361            0 :                 return all(ENGINE_FAILED);
    1362              :             }
    1363              :             Err(_) => {
    1364            0 :                 tracing::error!(
    1365              :                     "policy engine {} did not answer within {:?}; denying",
    1366            0 :                     engine.name(),
    1367            0 :                     *TIMEOUT
    1368              :                 );
    1369            0 :                 metrics::counter!("antares_policy_failures_total", "reason" => "timeout")
    1370            0 :                     .increment(1);
    1371            0 :                 return all(ENGINE_TIMED_OUT);
    1372              :             }
    1373              :         }
    1374              :     };
    1375              :     #[cfg(target_arch = "wasm32")]
    1376              :     let answer = ask().await;
    1377           88 :     if answer.len() != ops.len() {
    1378            6 :         tracing::error!(
    1379              :             "policy engine {} answered {} decisions for {} operations; denying",
    1380            0 :             engine.name(),
    1381            0 :             answer.len(),
    1382            0 :             ops.len()
    1383              :         );
    1384            6 :         metrics::counter!("antares_policy_failures_total", "reason" => "length").increment(1);
    1385            6 :         return all(ENGINE_FAILED);
    1386           82 :     }
    1387           82 :     answer
    1388           82 :         .into_iter()
    1389           82 :         .zip(ops)
    1390          296 :         .map(|(d, op)| resolve(op.clause, d))
    1391           82 :         .collect()
    1392           88 : }
    1393              : 
    1394              : /// Whether any request of each of `clauses` could be allowed for the caller
    1395              : /// of `headers`: one batch-shaped probe a surface lists its operations by
    1396              : /// (a surface's listing), never a decision on a request. Each clause is asked as an
    1397              : /// Operation that names nothing, through [`PolicyEngine::offers_each`],
    1398              : /// failing closed as [`decide_each`] does (a panic, a timeout or a short
    1399              : /// answer hides every clause), and a narrowing on a clause that cannot be
    1400              : /// narrowed hides it. With no engine attached every clause is offered.
    1401            8 : pub async fn offers(
    1402            8 :     engine: Option<&dyn PolicyEngine>,
    1403            8 :     tenant: &antares_model::TenantId,
    1404            8 :     headers: &HeaderMap,
    1405            8 :     clauses: &[&'static str],
    1406            8 : ) -> Vec<bool> {
    1407            8 :     let Some(engine) = engine else {
    1408            4 :         return vec![true; clauses.len()];
    1409              :     };
    1410            4 :     let ops: Vec<Operation<'_>> = clauses
    1411            4 :         .iter()
    1412            4 :         .map(|clause| Operation {
    1413           12 :             clause,
    1414           12 :             ids: &[],
    1415           12 :             types: &[],
    1416           12 :             attrs: &[],
    1417           12 :             q: None,
    1418           12 :             scope_q: None,
    1419           12 :             geo: None,
    1420           12 :             body: None,
    1421           12 :             stored: None,
    1422           12 :         })
    1423            4 :         .collect();
    1424            4 :     let subject = subject_of(tenant, headers);
    1425            4 :     guarded(engine, &ops, || engine.offers_each(&subject, &ops))
    1426            4 :         .await
    1427            4 :         .into_iter()
    1428           12 :         .map(|d| !matches!(d, Decision::Deny(_)))
    1429            4 :         .collect()
    1430            8 : }
    1431              : 
    1432              : /// Ask the engine about one notification, and fail closed: an engine that
    1433              : /// panics drops the notification. The document it was handed may already be
    1434              : /// half-edited, and the broker cannot know which half — so it is not sent.
    1435           52 : pub fn pre_notify(
    1436           52 :     engine: &dyn PolicyEngine,
    1437           52 :     subject: &Subject,
    1438           52 :     sub: &Value,
    1439           52 :     notification: &mut Value,
    1440           52 : ) -> NotifyDecision {
    1441           52 :     match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    1442           52 :         engine.pre_notify(subject, sub, notification)
    1443           52 :     })) {
    1444           48 :         Ok(d) => d,
    1445              :         Err(_) => {
    1446            4 :             tracing::error!(
    1447              :                 "policy engine {} panicked on a notification; dropping it",
    1448            0 :                 engine.name()
    1449              :             );
    1450            4 :             metrics::counter!("antares_policy_failures_total", "reason" => "panic").increment(1);
    1451            4 :             NotifyDecision::Drop
    1452              :         }
    1453              :     }
    1454           52 : }
    1455              : 
    1456              : /// The ProblemDetails `type` and `title` of a refusal. Table 6.3.2-1 names
    1457              : /// no access-denied error, so none is invented under the ETSI namespace: a
    1458              : /// refusal is answered with this broker's own URN, documented in the book
    1459              : /// and noted in the 6.3.2 ledger entry. A client that reads error types can
    1460              : /// tell a policy refusal from a spec error by the namespace alone.
    1461              : pub const ACCESS_DENIED_TYPE: &str = "urn:antares:error:AccessDenied";
    1462              : pub const ACCESS_DENIED_TITLE: &str = "AccessDenied";
    1463              : 
    1464              : /// The request headers copied into a [`Subject`], comma-separated
    1465              : /// (`ANTARES_POLICY_SUBJECT_HEADERS`), matched case-insensitively and read
    1466              : /// once at first use. Empty by default: an engine that wants an identity
    1467              : /// names the header that carries it, and a broker that copied every header
    1468              : /// into the subject would be handing an addon the whole request.
    1469           56 : pub static SUBJECT_HEADERS: LazyLock<Vec<String>> = LazyLock::new(|| {
    1470           56 :     std::env::var("ANTARES_POLICY_SUBJECT_HEADERS")
    1471           56 :         .unwrap_or_default()
    1472           56 :         .split(',')
    1473           56 :         .map(|s| s.trim().to_ascii_lowercase())
    1474           56 :         .filter(|s| !s.is_empty())
    1475           56 :         .collect()
    1476           56 : });
    1477              : 
    1478              : /// Who this request is from, as far as the seam is concerned: the tenant it
    1479              : /// addresses and the headers a deployment named. A header the request does
    1480              : /// not carry is simply absent — the seam invents nothing.
    1481         9204 : pub fn subject_of(tenant: &antares_model::TenantId, headers: &HeaderMap) -> Subject {
    1482         9204 :     let mut carried = Vec::new();
    1483         9204 :     for name in SUBJECT_HEADERS.iter() {
    1484          394 :         for value in headers.get_all(name.as_str()) {
    1485          280 :             if let Ok(v) = value.to_str() {
    1486          280 :                 carried.push((name.clone(), v.to_owned()));
    1487          280 :             }
    1488              :         }
    1489              :     }
    1490         9204 :     if let Ok(view) = VIEW.try_with(std::sync::Arc::clone) {
    1491            8 :         carried.push((VIEW_PAIR.to_owned(), view.to_string()));
    1492         9196 :     }
    1493         9204 :     Subject {
    1494         9204 :         tenant: tenant.clone(),
    1495         9204 :         headers: carried,
    1496         9204 :     }
    1497         9204 : }
    1498              : 
    1499              : /// The member a Subscription carries its creator's subject in. A
    1500              : /// broker-internal member like `__context` and `__via`: a client can
    1501              : /// neither set it nor read it back, it is stripped from every served
    1502              : /// representation and from the 5.8.1.4 copy forwarded to a Context Source,
    1503              : /// and 5.2.12 defines no such member — the whole `__` prefix is the
    1504              : /// broker's, so a member added later inherits every one of those rules
    1505              : /// instead of a new list to forget.
    1506              : pub(crate) const SUBJECT_MEMBER: &str = "__subject";
    1507              : 
    1508              : /// What a write by id hands the engine about the Entity it lands on.
    1509              : #[derive(Debug, Default)]
    1510              : pub(crate) struct PreImage {
    1511              :     /// The stored Entity's types joined with the ones the request body
    1512              :     /// names: an engine judging by type judges the Entity the write changes
    1513              :     /// and what it changes it to (ADR-0020).
    1514              :     pub types: Vec<String>,
    1515              :     /// The stored Entity itself ([`Operation::stored`]).
    1516              :     pub stored: Option<StoredImage>,
    1517              : }
    1518              : 
    1519              : /// The [`PreImage`] of a write by id. The store is read once per request,
    1520              : /// and only with an engine attached, so vanilla reads nothing more. An
    1521              : /// absent Entity or a store error gives no stored image, and the write
    1522              : /// answers it as before.
    1523              : ///
    1524              : /// Decided (worker-2, owner may override): the write still reads
    1525              : /// the Entity again inside its own store operation. Handing it this image,
    1526              : /// read outside the store's transaction, would turn a concurrent write
    1527              : /// into a lost update; the gate's read is the one the type lookup already
    1528              : /// made, so no read is added.
    1529        17744 : pub(crate) async fn pre_image(
    1530        17744 :     st: &crate::state::AppState,
    1531        17744 :     tenant: &antares_model::TenantId,
    1532        17744 :     id: &str,
    1533        17744 :     body: Option<&Value>,
    1534        17744 : ) -> Result<PreImage, antares_model::NgsiError> {
    1535        17744 :     if st.policy.is_none() {
    1536        17460 :         return Ok(PreImage::default());
    1537          284 :     }
    1538              :     // A failed read is the request's failure, never an absent Entity: a
    1539              :     // missing pre-image would let a rule keyed on the stored types or
    1540              :     // headers decide over nothing (fail-closed, ADR-0020).
    1541          284 :     let stored = st
    1542          284 :         .store
    1543          284 :         .get(tenant, antares_store::Kind::Entity, id)
    1544          284 :         .await?;
    1545          282 :     Ok(image(stored, body))
    1546        17744 : }
    1547              : 
    1548              : /// The [`PreImage`] of a temporal write by id (5.6.12 to 5.6.16):
    1549              : /// the Entity's temporal evolution, which a history-only Entity has
    1550              : /// without a current state, else its current state. Read only with an
    1551              : /// engine attached, as [`pre_image`].
    1552         3760 : pub(crate) async fn temporal_pre_image(
    1553         3760 :     st: &crate::state::AppState,
    1554         3760 :     tenant: &antares_model::TenantId,
    1555         3760 :     id: &str,
    1556         3760 : ) -> Result<PreImage, antares_model::NgsiError> {
    1557         3760 :     if st.policy.is_none() {
    1558         3664 :         return Ok(PreImage::default());
    1559           96 :     }
    1560           96 :     match st.temporal.get(tenant, id).await? {
    1561           56 :         Some(evolution) => Ok(image(Some(evolution), None)),
    1562           38 :         None => pre_image(st, tenant, id, None).await,
    1563              :     }
    1564         3760 : }
    1565              : 
    1566              : /// The image of a stored document, with the types the body adds.
    1567          606 : pub(crate) fn image(stored: Option<Value>, body: Option<&Value>) -> PreImage {
    1568          606 :     let mut types: Vec<String> = Vec::new();
    1569          648 :     for doc in stored.as_ref().into_iter().chain(body) {
    1570          650 :         for t in entity_types(doc) {
    1571          554 :             if !types.iter().any(|o| o == t) {
    1572          498 :                 types.push(t.to_owned());
    1573          498 :             }
    1574              :         }
    1575              :     }
    1576          606 :     PreImage {
    1577          606 :         types,
    1578          606 :         stored: stored.as_ref().map(StoredImage::of),
    1579          606 :     }
    1580          606 : }
    1581              : 
    1582              : /// The stored form of a subject: only the headers a deployment named, since
    1583              : /// the tenant is where the subscription already lives. `None` when the
    1584              : /// subject carries nothing, so a deployment that named no header stores no
    1585              : /// member at all.
    1586         5510 : pub(crate) fn subject_member(subject: &Subject) -> Option<Value> {
    1587         5510 :     (!subject.headers.is_empty())
    1588         5510 :         .then(|| serde_json::to_value(&subject.headers).ok())
    1589         5510 :         .flatten()
    1590         5510 : }
    1591              : 
    1592              : /// The subject a notification is delivered under: the one stored when the
    1593              : /// subscription was created. A subscription created before the deployment
    1594              : /// named its headers — or by the broker itself, for the 5.8.1.4 internal
    1595              : /// copies — simply has none, and the engine is asked about a subject with
    1596              : /// no headers rather than about the wrong one.
    1597          278 : pub(crate) fn stored_subject(tenant: &antares_model::TenantId, sub: &Value) -> Subject {
    1598              :     Subject {
    1599          278 :         tenant: tenant.clone(),
    1600          278 :         headers: sub
    1601          278 :             .get(SUBJECT_MEMBER)
    1602          278 :             .cloned()
    1603          278 :             .and_then(|v| serde_json::from_value(v).ok())
    1604          278 :             .unwrap_or_default(),
    1605              :     }
    1606          278 : }
    1607              : 
    1608              : /// Remove every broker-internal member from a document about to be served.
    1609              : ///
    1610              : /// The `__` prefix is the broker's: the notification `@context` (5.8.6), the
    1611              : /// 6.3.18 Via chain, a snapshot's synthetic tenant and [`SUBJECT_MEMBER`]
    1612              : /// all live under it, and no NGSI-LD data type defines a member there. One
    1613              : /// predicate rather than a list per document type, so a member added later
    1614              : /// is hidden by construction instead of by remembering every serve point.
    1615          508 : pub(crate) fn strip_internal(doc: &mut Value) {
    1616          508 :     if let Some(o) = doc.as_object_mut() {
    1617         4403 :         o.retain(|k, _| !k.starts_with("__"));
    1618            0 :     }
    1619          508 : }
    1620              : 
    1621              : /// Whether a stored document was made by this subject. Compared on the
    1622              : /// headers alone: the tenant is where the document already lives, and a
    1623              : /// deployment that named no header has no subjects to tell apart — every
    1624              : /// request is the same one, and everything it stored is its own.
    1625          238 : pub(crate) fn belongs_to(doc: &Value, subject: &Subject) -> bool {
    1626          238 :     stored_subject(&subject.tenant, doc).headers == subject.headers
    1627          238 : }
    1628              : 
    1629              : /// A refusal on its way out of a handler. Its own type rather than an
    1630              : /// `ApiError`, so this module names nothing above it and stays a leaf; the
    1631              : /// `?` in a handler converts it through `From`.
    1632              : #[derive(Debug, Clone, PartialEq)]
    1633              : pub struct Denied(pub String);
    1634              : 
    1635              : /// The one call a handler makes. Every operation passes through it exactly
    1636              : /// once, which is what `every_route_asks_the_policy_engine_exactly_once` asserts by
    1637              : /// walking the router with a counting engine.
    1638              : ///
    1639              : /// The returned [`Filter`] is what the answer has to be narrowed by; an
    1640              : /// allow returns the empty filter, which narrows nothing.
    1641         3376 : pub async fn gate(
    1642         3376 :     engine: &dyn PolicyEngine,
    1643         3376 :     tenant: &antares_model::TenantId,
    1644         3376 :     headers: &HeaderMap,
    1645         3376 :     op: &Operation<'_>,
    1646         3376 : ) -> Result<Filter, Denied> {
    1647         3376 :     match decide(engine, &subject_of(tenant, headers), op).await {
    1648         2346 :         Decision::Allow => Ok(Filter::default()),
    1649              :         // On a write by id the filter is the condition ([`Condition::of`]);
    1650              :         // `resolve` lets an `AllowIf` through nowhere else, and turns a
    1651              :         // `Filter` there into a deny, so a handler of such a clause reads
    1652              :         // what it gets back as the condition and nothing else.
    1653          910 :         Decision::Filter(f) | Decision::AllowIf(f) => Ok(f),
    1654          120 :         Decision::Deny(why) => Err(Denied(why)),
    1655              :     }
    1656         3376 : }
    1657              : 
    1658              : /// [`gate`](fn@gate) for a batch: per member the refusal reason, or the
    1659              : /// condition its stored Entity has to meet (empty when there is none). A
    1660              : /// batch writes its members whole, so a `Filter` on a member is a refusal
    1661              : /// ([`resolve`]: no batch clause is FILTERABLE).
    1662           76 : pub async fn gate_each(
    1663           76 :     engine: &dyn PolicyEngine,
    1664           76 :     tenant: &antares_model::TenantId,
    1665           76 :     headers: &HeaderMap,
    1666           76 :     ops: &[Operation<'_>],
    1667           76 : ) -> Vec<Result<Filter, String>> {
    1668           76 :     decide_each(engine, &subject_of(tenant, headers), ops)
    1669           76 :         .await
    1670           76 :         .into_iter()
    1671          280 :         .map(|d| match d {
    1672           28 :             Decision::Deny(why) => Err(why),
    1673           36 :             Decision::AllowIf(f) => Ok(f),
    1674          216 :             Decision::Allow | Decision::Filter(_) => Ok(Filter::default()),
    1675          280 :         })
    1676           76 :         .collect()
    1677           76 : }
    1678              : 
    1679              : /// A stored Subscription or Registration as a [`Condition`] reads it: `id`,
    1680              : /// `type` and `scope` as an Entity carries them, every other member one
    1681              : /// Property holding its value, and `subject` the carried header values of
    1682              : /// the subject that created it ([`SUBJECT_MEMBER`]), one instance each, so
    1683              : /// an engine selects the caller's own with `q=subject=="<its value>"`. A
    1684              : /// member a client wrote under `subject` is not the subject and is left
    1685              : /// out, as are the broker's other `__` members.
    1686              : // Values only, without the header names; an engine whose two
    1687              : // headers can carry the same value tells them apart by a rule's ids.
    1688          238 : fn record_image(doc: &Value) -> Value {
    1689          238 :     let ctx = crate::repr::policy_context();
    1690          238 :     let mut out = serde_json::Map::new();
    1691         1916 :     for (k, v) in doc.as_object().into_iter().flatten() {
    1692         1916 :         match k.as_str() {
    1693         1916 :             "id" | "scope" => {
    1694          238 :                 out.insert(k.clone(), v.clone());
    1695          238 :             }
    1696         1678 :             "type" => {
    1697          238 :                 let t = v
    1698          238 :                     .as_str()
    1699          238 :                     .map_or_else(|| v.clone(), |t| ctx.expand_key(t).into());
    1700          238 :                 out.insert(k.clone(), t);
    1701              :             }
    1702         1440 :             "subject" => {}
    1703         1404 :             k if k.starts_with("__") => {}
    1704         1046 :             k => {
    1705         1046 :                 out.insert(ctx.expand_key(k), serde_json::json!([{ "value": v }]));
    1706         1046 :             }
    1707              :         }
    1708              :     }
    1709          238 :     let mut carried: Vec<(String, String)> = doc
    1710          238 :         .get(SUBJECT_MEMBER)
    1711          238 :         .cloned()
    1712          238 :         .and_then(|v| serde_json::from_value(v).ok())
    1713          238 :         .unwrap_or_default();
    1714              :     // The view is where the creator came from, not who they are: a view
    1715              :     // id equal to some user's name must not select that user's documents.
    1716          238 :     carried.retain(|(name, _)| name != VIEW_PAIR);
    1717          238 :     if !carried.is_empty() {
    1718          234 :         let values = carried
    1719          234 :             .into_iter()
    1720          234 :             .map(|(_, v)| serde_json::json!({ "value": v }));
    1721          234 :         out.insert(ctx.expand_key("subject"), values.collect());
    1722            4 :     }
    1723          238 :     Value::Object(out)
    1724          238 : }
    1725              : 
    1726              : /// What a write by id needs of the Entity it lands on
    1727              : /// ([`Decision::AllowIf`]), in the form a stored document is read in. The
    1728              : /// handler checks it inside the store's own read-modify-write, on the
    1729              : /// document the store is about to change.
    1730              : ///
    1731              : /// The same selection a read's narrowing makes ([`crate::repr::PolicyReach`]),
    1732              : /// judged synchronously on one document.
    1733              : pub(crate) type Condition = crate::repr::PolicyReach;
    1734              : 
    1735              : impl Condition {
    1736              :     /// The selection `f` makes, whatever it projects.
    1737        25321 :     pub(crate) fn new(f: &Filter) -> Self {
    1738        25321 :         Self {
    1739        25321 :             q: f.q.as_deref().cloned(),
    1740        25321 :             scope_q: f.scope_q.clone(),
    1741        25321 :             areas: f.areas().cloned().collect(),
    1742        25321 :             rules: crate::repr::type_rules(f),
    1743        25321 :         }
    1744        25321 :     }
    1745              : 
    1746              :     /// The condition `f` sets, or `None` when it selects every Entity.
    1747        25321 :     pub(crate) fn of(f: &Filter) -> Option<Self> {
    1748        25321 :         let c = Self::new(f);
    1749        25321 :         (!c.selects_every()).then_some(c)
    1750        25321 :     }
    1751              : 
    1752              :     /// Whether the condition selects every Entity: no `q`, no `scopeQ`, no
    1753              :     /// area and no rules.
    1754        25321 :     pub(crate) fn selects_every(&self) -> bool {
    1755        25321 :         self.q.is_none() && self.scope_q.is_none() && self.areas.is_empty() && self.rules.is_none()
    1756        25321 :     }
    1757              : 
    1758              :     /// Whether the stored document `doc` is selected. A linked path in a
    1759              :     /// `q` never holds, as in a rule: the check runs
    1760              :     /// inside the store's write and reads no other Entity.
    1761          368 :     pub(crate) fn holds(&self, doc: &Value) -> bool {
    1762          368 :         let ctx = crate::repr::policy_context();
    1763          368 :         crate::repr::shown(self.rules.as_deref(), doc).is_some()
    1764          362 :             && self
    1765          362 :                 .scope_q
    1766          362 :                 .as_deref()
    1767          362 :                 .is_none_or(|s| crate::scope_matches(s, doc))
    1768          362 :             && self.areas.iter().all(|g| g.matches(doc, ctx))
    1769          362 :             && self.q.as_ref().is_none_or(|q| {
    1770          356 :                 !q.has_linked_paths() && antares_ql::eval::eval_q(q, doc, ctx, &|_| None)
    1771          356 :             })
    1772          368 :     }
    1773              : 
    1774              :     /// Whether a Subscription or Registration is selected: the
    1775              :     /// condition read over [`record_image`] of the stored document.
    1776          230 :     pub(crate) fn holds_for_record(&self, doc: &Value) -> bool {
    1777          230 :         self.holds(&record_image(doc))
    1778          230 :     }
    1779              : 
    1780              :     /// Inside the store closure: an Entity the condition does not select is
    1781              :     /// not found, the answer an absent one gets.
    1782        14242 :     pub(crate) fn pre(
    1783        14242 :         cond: Option<&Self>,
    1784        14242 :         id: &str,
    1785        14242 :         doc: &Value,
    1786        14242 :     ) -> Result<(), antares_model::NgsiError> {
    1787        14242 :         if cond.is_none_or(|c| c.holds(doc)) {
    1788        14208 :             Ok(())
    1789              :         } else {
    1790           34 :             Err(antares_model::NgsiError::ResourceNotFound(format!(
    1791           34 :                 "entity {id} not found"
    1792           34 :             )))
    1793              :         }
    1794        14242 :     }
    1795              : 
    1796              :     /// [`Condition::pre`] on a Temporal Evolution, inside the temporal
    1797              :     /// store's closure: judged on [`latest_image`], and one it does not
    1798              :     /// select is the absent evolution the temporal writes answer.
    1799          110 :     pub(crate) fn pre_evolution(
    1800          110 :         cond: Option<&Self>,
    1801          110 :         id: &str,
    1802          110 :         doc: &Value,
    1803          110 :     ) -> Result<(), antares_model::NgsiError> {
    1804          110 :         if cond.is_none_or(|c| c.holds(&latest_image(doc))) {
    1805           94 :             Ok(())
    1806              :         } else {
    1807           16 :             Err(antares_model::NgsiError::ResourceNotFound(format!(
    1808           16 :                 "temporal entity {id} not found"
    1809           16 :             )))
    1810              :         }
    1811          110 :     }
    1812              : 
    1813              :     /// A conditioned write that would reach a Context Source is refused:
    1814              :     /// nothing in this store can judge the Source's copy.
    1815        17808 :     pub(crate) fn unforwarded(cond: Option<&Self>, forwards: bool) -> Result<(), Denied> {
    1816        17808 :         if cond.is_some() && forwards {
    1817            8 :             Err(Denied(CONDITION_NOT_FORWARDED.to_owned()))
    1818              :         } else {
    1819        17800 :             Ok(())
    1820              :         }
    1821        17808 :     }
    1822              : }
    1823              : 
    1824              : /// A Temporal Evolution as the Entity it now is: each Attribute keeps its
    1825              : /// latest instance per `datasetId` (the greatest `observedAt`, else
    1826              : /// `createdAt`; the later one on a tie) and a temporal `scope` the value of
    1827              : /// its latest instance. A condition judged on every instance would let a
    1828              : /// value the Entity held once select it for good.
    1829              : // Instances without a time compare as the earliest; the temporal
    1830              : // writes stamp `createdAt` on every instance they store.
    1831           32 : pub(crate) fn latest_image(doc: &Value) -> Value {
    1832           96 :     let time = |i: &Value| {
    1833           96 :         ["observedAt", "createdAt"]
    1834           96 :             .iter()
    1835           96 :             .find_map(|k| i.get(*k).and_then(Value::as_str))
    1836           96 :             .map(antares_model::dt_key)
    1837           96 :             .unwrap_or_default()
    1838           96 :     };
    1839           32 :     let mut out = serde_json::Map::new();
    1840          172 :     for (k, v) in doc.as_object().into_iter().flatten() {
    1841          172 :         let instances = match v.as_array() {
    1842           84 :             Some(a) if !antares_model::is_meta(k) || k == "scope" => a,
    1843              :             _ => {
    1844          116 :                 out.insert(k.clone(), v.clone());
    1845          116 :                 continue;
    1846              :             }
    1847              :         };
    1848           56 :         if !instances.iter().all(Value::is_object) {
    1849            0 :             out.insert(k.clone(), v.clone());
    1850            0 :             continue;
    1851           56 :         }
    1852           56 :         let mut latest: Vec<&Value> = Vec::new();
    1853          108 :         for i in instances {
    1854          108 :             let ds = i.get("datasetId");
    1855          108 :             match latest.iter_mut().find(|l| l.get("datasetId") == ds) {
    1856           48 :                 Some(l) if time(i) >= time(l) => *l = i,
    1857           28 :                 Some(_) => {}
    1858           60 :                 None => latest.push(i),
    1859              :             }
    1860              :         }
    1861           56 :         let kept = if k == "scope" {
    1862            4 :             latest
    1863            4 :                 .first()
    1864            4 :                 .and_then(|i| i.get("value"))
    1865            4 :                 .cloned()
    1866            4 :                 .unwrap_or(Value::Null)
    1867              :         } else {
    1868           52 :             Value::Array(latest.into_iter().cloned().collect())
    1869              :         };
    1870           56 :         out.insert(k.clone(), kept);
    1871              :     }
    1872           32 :     Value::Object(out)
    1873           32 : }
    1874              : 
    1875              : /// The members of a JSON object, or nothing when it is not one.
    1876              : #[cfg(any(test, feature = "test-kit"))]
    1877           52 : fn members(v: &Value) -> Vec<String> {
    1878           52 :     v.as_object()
    1879           52 :         .map(|o| o.keys().cloned().collect())
    1880           52 :         .unwrap_or_default()
    1881           52 : }
    1882              : 
    1883              : /// The contract every engine has to hold, so an addon's own tests can call
    1884              : /// it. It asserts the three things an engine can actually get wrong — it
    1885              : /// stops answering, it hands back an answer the seam has to override, or it
    1886              : /// puts something into a notification that was not there — and it asserts
    1887              : /// them through the seam, so an engine that passes here passes as the
    1888              : /// broker will call it.
    1889              : ///
    1890              : /// The core runs it against [`AllowAll`]; `examples/plugin-example` runs it
    1891              : /// against the reference engine.
    1892              : #[cfg(any(test, feature = "test-kit"))]
    1893           12 : pub async fn run_policy_contract(engine: &dyn PolicyEngine) {
    1894           12 :     let name = engine.name();
    1895           12 :     assert!(!name.is_empty(), "an engine answers to a name");
    1896              : 
    1897           12 :     let subject = Subject {
    1898           12 :         tenant: antares_model::TenantId::default(),
    1899           12 :         headers: vec![("X-Subject".into(), "someone".into())],
    1900           12 :     };
    1901           12 :     let doc = serde_json::json!({
    1902           12 :         "id": "urn:ngsi-ld:Vehicle:1",
    1903           12 :         "type": "Vehicle",
    1904           12 :         "speed": {"type": "Property", "value": 10},
    1905           12 :         "brand": {"type": "Property", "value": "Skoda"}
    1906              :     });
    1907              : 
    1908              :     // The engine answers, and the answer is its own: a deny carrying one of
    1909              :     // the seam's own reasons means it timed out or panicked instead.
    1910           36 :     for clause in ["5.6.1", "5.7.2", "5.8.1"] {
    1911           36 :         match decide(engine, &subject, &Operation::new(clause)).await {
    1912           24 :             Decision::Allow => {}
    1913            8 :             Decision::Deny(why) => assert!(
    1914            8 :                 why != ENGINE_TIMED_OUT && why != ENGINE_FAILED,
    1915              :                 "{name}: {clause} was denied by the seam, not by the engine: {why}"
    1916              :             ),
    1917            4 :             Decision::Filter(f) => {
    1918            4 :                 let mut narrowed = doc.clone();
    1919            4 :                 f.project(&mut narrowed);
    1920            4 :                 let before = members(&doc);
    1921           12 :                 for key in members(&narrowed) {
    1922           12 :                     assert!(
    1923           12 :                         before.contains(&key),
    1924              :                         "{name}: the filter for {clause} added {key:?} to the answer"
    1925              :                     );
    1926              :                 }
    1927              :             }
    1928              :             // `decide` answers a condition on anything but a write by id as
    1929              :             // a deny, so reaching here is the seam's fault, not the engine's.
    1930              :             // A contract harness (test-kit) fails by panicking, as asserts do.
    1931              :             #[allow(clippy::panic)]
    1932            0 :             Decision::AllowIf(_) => panic!("{name}: a condition on {clause} passed the seam"),
    1933              :         }
    1934              :     }
    1935              : 
    1936              :     // An operation the broker performs whole is allowed or refused, never
    1937              :     // done to less than it says: WHOLE_TENANT because there is no narrowed
    1938              :     // form of "delete everything", and the writes because no handler there
    1939              :     // reads a Filter at all (FILTERABLE).
    1940           96 :     for clause in WHOLE_TENANT
    1941           12 :         .iter()
    1942           12 :         .chain(["5.6.1", "5.6.6", "5.8.1", "5.9.2"].iter())
    1943              :     {
    1944           96 :         assert!(
    1945           96 :             !matches!(
    1946           96 :                 decide(engine, &subject, &Operation::new(clause)).await,
    1947              :                 Decision::Filter(_)
    1948              :             ),
    1949              :             "{name}: {clause} was answered with a filter"
    1950              :         );
    1951              :     }
    1952              : 
    1953              :     // `pre_notify` holds the notification by `&mut`, which is the one place
    1954              :     // an engine can widen rather than narrow: a member it puts there is a
    1955              :     // member no subscriber asked for and no store answered with.
    1956           12 :     let sub = serde_json::json!({"id": "urn:ngsi-ld:Subscription:1", "type": "Subscription"});
    1957           12 :     let mut notification = serde_json::json!({
    1958           12 :         "id": "urn:ngsi-ld:Notification:1",
    1959           12 :         "type": "Notification",
    1960           12 :         "subscriptionId": "urn:ngsi-ld:Subscription:1",
    1961           12 :         "notifiedAt": "2026-01-01T00:00:00Z",
    1962           12 :         "data": [doc.clone()]
    1963              :     });
    1964           12 :     let before = members(&notification);
    1965           12 :     let entity_members = members(&doc);
    1966           12 :     if pre_notify(engine, &subject, &sub, &mut notification) != NotifyDecision::Drop {
    1967           56 :         for key in members(&notification) {
    1968           56 :             assert!(
    1969           56 :                 before.contains(&key),
    1970              :                 "{name}: pre_notify added {key:?} to the notification"
    1971              :             );
    1972              :         }
    1973            8 :         let served = notification
    1974            8 :             .get("data")
    1975            8 :             .and_then(Value::as_array)
    1976            8 :             .cloned()
    1977            8 :             .unwrap_or_default();
    1978            8 :         for entity in &served {
    1979           28 :             for key in members(entity) {
    1980           28 :                 assert!(
    1981           28 :                     entity_members.contains(&key),
    1982              :                     "{name}: pre_notify added {key:?} to a notified Entity"
    1983              :                 );
    1984              :             }
    1985              :         }
    1986            0 :     }
    1987              : 
    1988              :     // And the seam stops waiting for this engine, whatever it does.
    1989              :     #[cfg(not(target_arch = "wasm32"))]
    1990              :     {
    1991            8 :         let slow = Slow(engine);
    1992            8 :         assert_eq!(
    1993            8 :             decide(&slow, &subject, &Operation::new("5.6.1")).await,
    1994            8 :             Decision::Deny(ENGINE_TIMED_OUT.to_owned()),
    1995              :             "{name}: the seam waited past its timeout instead of denying"
    1996              :         );
    1997              :     }
    1998            8 : }
    1999              : 
    2000              : /// The same engine, one timeout slower: what the contract wraps it in to
    2001              : /// prove the seam stops waiting.
    2002              : #[cfg(all(any(test, feature = "test-kit"), not(target_arch = "wasm32")))]
    2003              : struct Slow<'e>(&'e dyn PolicyEngine);
    2004              : 
    2005              : #[cfg(all(any(test, feature = "test-kit"), not(target_arch = "wasm32")))]
    2006              : impl PolicyEngine for Slow<'_> {
    2007            0 :     fn name(&self) -> &str {
    2008            0 :         self.0.name()
    2009            0 :     }
    2010              : 
    2011            8 :     fn decide<'a>(&'a self, subject: &'a Subject, op: &'a Operation<'a>) -> DecisionFuture<'a> {
    2012            8 :         Box::pin(async move {
    2013            8 :             tokio::time::sleep(
    2014            8 :                 TIMEOUT
    2015            8 :                     .saturating_mul(2)
    2016            8 :                     .saturating_add(Duration::from_millis(50)),
    2017            8 :             )
    2018            8 :             .await;
    2019            0 :             self.0.decide(subject, op).await
    2020            0 :         })
    2021            8 :     }
    2022              : 
    2023            0 :     fn pre_notify(&self, s: &Subject, sub: &Value, n: &mut Value) -> NotifyDecision {
    2024            0 :         self.0.pre_notify(s, sub, n)
    2025            0 :     }
    2026              : }
    2027              : 
    2028              : /// The most id segments a probe narrows by; past them it narrows by the
    2029              : /// literal alone.
    2030              : const MAX_ID_SEGMENTS: usize = 64;
    2031              : 
    2032              : /// The exact id prefixes a rule's one id pattern forces, from the segments
    2033              : /// the Tenant's ids show ([`antares_store::filter::id_pattern_segments`]):
    2034              : /// the store reads them as ranges of an index in their own order, where the
    2035              : /// literal of `types_attrs::id_narrowing` filtered a walk of the whole type, which for
    2036              : /// a few late matches still took 10 s on a deployment. `None` when the
    2037              : /// pattern does not split or the store keeps no segments.
    2038          672 : pub(crate) async fn id_prefixes(
    2039          672 :     st: &crate::state::AppState,
    2040          672 :     tenant: &antares_model::TenantId,
    2041          672 :     rule: Option<&Rule>,
    2042          672 : ) -> Result<Option<Vec<String>>, antares_model::NgsiError> {
    2043           16 :     let Some(seg) =
    2044          672 :         rule.filter(|r| r.ids.is_empty())
    2045          672 :             .and_then(|r| match r.id_patterns.as_slice() {
    2046           28 :                 [p] => antares_store::filter::id_pattern_segments(p),
    2047          410 :                 _ => None,
    2048          438 :             })
    2049              :     else {
    2050          656 :         return Ok(None);
    2051              :     };
    2052           16 :     Ok(st
    2053           16 :         .store
    2054           16 :         .entity_id_segments(tenant, &seg.lead, seg.sep, MAX_ID_SEGMENTS)
    2055           16 :         .await?
    2056           16 :         .map(|segs| {
    2057           16 :             segs.into_iter()
    2058           20 :                 .map(|s| format!("{s}{}", seg.tail))
    2059           16 :                 .collect()
    2060           16 :         }))
    2061          672 : }
    2062              : 
    2063              : /// The exact id prefixes every rule of `rules` forces, united: a necessary
    2064              : /// condition of the rules' OR, as [`id_prefixes`] is of one rule. A query
    2065              : /// under a dataset view whose Policy narrows by one `idPattern` walked every
    2066              : /// Entity of the type for its few matches, the regex judged per row: 10 s and
    2067              : /// the gateway's 503 for `limit=1` over 128 678 KPIs.
    2068              : /// `None` when a rule that holds for anything does not split.
    2069          256 : pub(crate) async fn rules_id_prefixes(
    2070          256 :     st: &crate::state::AppState,
    2071          256 :     tenant: &antares_model::TenantId,
    2072          256 :     rules: &[Rule],
    2073          256 : ) -> Result<Option<Vec<String>>, antares_model::NgsiError> {
    2074          256 :     let mut all = Vec::new();
    2075              :     // a rule naming no type holds for nothing (`Rule::types`)
    2076          256 :     for rule in rules.iter().filter(|r| !r.types.is_empty()) {
    2077          254 :         match id_prefixes(st, tenant, Some(rule)).await? {
    2078           12 :             Some(ps) => all.extend(ps),
    2079          242 :             None => return Ok(None),
    2080              :         }
    2081              :     }
    2082           14 :     Ok(Some(all).filter(|a| !a.is_empty()))
    2083          256 : }
    2084              : 
    2085              : /// The `idPattern`s every rule of `rules` that holds for anything narrows
    2086              : /// by, when that is all they narrow by beside their types and they all name
    2087              : /// the same ones, each of which the store applies exactly
    2088              : /// ([`antares_store::filter::id_pattern_sql`]): then the rules hold for
    2089              : /// exactly the Entities of their types whose id matches one, and the store
    2090              : /// decides, pages and counts them. A view's Policy often narrows its type
    2091              : /// by one such pattern; judged per row, a count over 164 000 Trees
    2092              : /// took past the gateway's 10 s.
    2093          354 : pub(crate) fn shared_id_patterns(rules: &[Rule]) -> Option<&[String]> {
    2094          448 :     let mut typed = rules.iter().filter(|r| !r.types.is_empty());
    2095          354 :     let first = typed.next()?;
    2096          352 :     let narrows =
    2097          440 :         |r: &Rule| !r.is_conditional() && r.ids.is_empty() && r.id_patterns == first.id_patterns;
    2098          352 :     let exact = |p: &String| {
    2099           22 :         antares_store::filter::id_pattern_sql(p).is_some()
    2100           18 :             && antares_ql::regex::compile(&format!("^(?:{p})$")).is_ok()
    2101           22 :     };
    2102          352 :     (narrows(first)
    2103          134 :         && typed.all(narrows)
    2104          114 :         && !first.id_patterns.is_empty()
    2105           22 :         && first.id_patterns.iter().all(exact))
    2106          352 :     .then_some(first.id_patterns.as_slice())
    2107          354 : }
    2108              : 
    2109              : #[cfg(test)]
    2110              : mod tests {
    2111              :     use super::*;
    2112              : 
    2113              :     /// `admits` is `shown(..).is_some()` without the set.
    2114              :     #[test]
    2115            4 :     fn admits_is_whether_shown_answers() {
    2116            4 :         let ctx = antares_jsonld::core_context();
    2117            4 :         let car = serde_json::json!({"id": "urn:ngsi-ld:Car:1", "type": "Car"});
    2118            4 :         let rule = |t: &str| Rule {
    2119           16 :             types: vec![t.to_owned()],
    2120           16 :             ..Rule::default()
    2121           16 :         };
    2122            4 :         let cases = [
    2123            4 :             Filter::default(),
    2124            4 :             Filter {
    2125            4 :                 omit_by_type: BTreeMap::from([("Car".to_owned(), vec!["speed".to_owned()])]),
    2126            4 :                 ..Filter::default()
    2127            4 :             },
    2128            4 :             Filter {
    2129            4 :                 rules: vec![rule("Car")],
    2130            4 :                 ..Filter::default()
    2131            4 :             },
    2132            4 :             Filter {
    2133            4 :                 rules: vec![rule("User")],
    2134            4 :                 ..Filter::default()
    2135            4 :             },
    2136            4 :             Filter {
    2137            4 :                 rules: vec![rule("User"), rule("Car")],
    2138            4 :                 ..Filter::default()
    2139            4 :             },
    2140            4 :         ];
    2141           20 :         let admitted: Vec<bool> = cases.iter().map(|f| f.admits(&car, &ctx)).collect();
    2142            4 :         assert_eq!(admitted, [true, true, true, false, true]);
    2143           20 :         for f in &cases {
    2144           20 :             assert_eq!(f.admits(&car, &ctx), f.shown(&car, &ctx).is_some(), "{f:?}");
    2145              :         }
    2146            4 :     }
    2147              : 
    2148              :     /// an engine that keeps the trait's defaults reads no rule
    2149              :     /// documents (ADR-0020 amendment): it says so, so `/q/policy` answers
    2150              :     /// 404, and a document handed to it anyway is refused, never taken.
    2151              :     #[test]
    2152            4 :     fn an_engine_on_the_defaults_reads_no_rule_documents() {
    2153            4 :         assert!(!AllowAll.loads_documents());
    2154            4 :         let refused = AllowAll.load("city", &serde_json::json!({"rules": []}));
    2155            4 :         assert!(
    2156            4 :             refused
    2157            4 :                 .as_ref()
    2158            4 :                 .is_err_and(|why| why.contains(BUILT_IN_NAME)),
    2159              :             "{refused:?}"
    2160              :         );
    2161            4 :     }
    2162              : 
    2163              :     /// `offers` answers one verdict per clause, in the clauses'
    2164              :     /// order, false for a Deny; with no engine every clause is offered.
    2165              :     #[tokio::test]
    2166            4 :     async fn offers_answers_each_clause_in_order() {
    2167              :         struct DenyRetrieve;
    2168              :         impl PolicyEngine for DenyRetrieve {
    2169            0 :             fn name(&self) -> &str {
    2170            0 :                 "deny-retrieve"
    2171            0 :             }
    2172           12 :             fn decide<'a>(&'a self, _s: &'a Subject, op: &'a Operation<'a>) -> DecisionFuture<'a> {
    2173           12 :                 Box::pin(std::future::ready(if op.clause == "retrieveEntity" {
    2174            4 :                     Decision::Deny("no".into())
    2175              :                 } else {
    2176            8 :                     Decision::Allow
    2177              :                 }))
    2178           12 :             }
    2179            0 :             fn pre_notify(&self, _s: &Subject, _sub: &Value, _n: &mut Value) -> NotifyDecision {
    2180            0 :                 NotifyDecision::Deliver
    2181            0 :             }
    2182              :         }
    2183            4 :         let (tenant, headers) = (antares_model::TenantId::default(), HeaderMap::new());
    2184            4 :         let clauses = ["queryEntity", "retrieveEntity", "queryTemporal"];
    2185            4 :         assert_eq!(
    2186            4 :             offers(Some(&DenyRetrieve), &tenant, &headers, &clauses).await,
    2187              :             [true, false, true]
    2188              :         );
    2189            4 :         assert_eq!(offers(None, &tenant, &headers, &clauses).await, [true; 3]);
    2190            4 :     }
    2191              : 
    2192              :     /// only the caller's own q is rewritten; the engine's
    2193              :     /// condition may read what it hides from the caller.
    2194              :     #[test]
    2195            4 :     fn a_hidden_term_is_rewritten_and_the_engines_own_q_is_not() {
    2196            4 :         let filter = Filter {
    2197            4 :             omit: vec!["salary".into()],
    2198            4 :             q: Some(Box::new(antares_ql::parse_q("salary>0").expect("q"))),
    2199            4 :             ..Filter::default()
    2200            4 :         };
    2201            4 :         let ctx = antares_jsonld::core_context();
    2202            4 :         let params = std::collections::HashMap::from([(
    2203            4 :             "q".to_owned(),
    2204            4 :             "salary==1|!salary|name==\"x\"".to_owned(),
    2205            4 :         )]);
    2206            4 :         let out = filter.narrow_params(&params, &ctx).expect("narrowed");
    2207            4 :         assert_eq!(
    2208            4 :             out["q"],
    2209              :             "(salary;!salary|(salary|!salary)|name==\"x\");salary>0"
    2210              :         );
    2211            4 :         let nothing_hidden = Filter::default();
    2212            4 :         let out = nothing_hidden
    2213            4 :             .narrow_params(&params, &ctx)
    2214            4 :             .expect("narrowed");
    2215            4 :         assert_eq!(out["q"], params["q"]);
    2216            4 :     }
    2217              :     use futures_util::FutureExt as _;
    2218              : 
    2219              :     const NS: &str = "https://uri.etsi.org/ngsi-ld/default-context/";
    2220              : 
    2221          712 :     fn iri(n: &str) -> String {
    2222          712 :         format!("{NS}{n}")
    2223          712 :     }
    2224              : 
    2225              :     /// Zone A: the square (0,0)-(1,1).
    2226           44 :     fn zone_a() -> Box<GeoQuery> {
    2227           44 :         let params = std::collections::HashMap::from([
    2228           44 :             ("georel".to_owned(), "within".to_owned()),
    2229           44 :             ("geometry".to_owned(), "Polygon".to_owned()),
    2230           44 :             (
    2231           44 :                 "coordinates".to_owned(),
    2232           44 :                 "[[[0,0],[1,0],[1,1],[0,1],[0,0]]]".to_owned(),
    2233           44 :             ),
    2234           44 :         ]);
    2235           44 :         Box::new(
    2236           44 :             GeoQuery::from_params(&params)
    2237           44 :                 .expect("geoquery")
    2238           44 :                 .expect("present"),
    2239              :         )
    2240           44 :     }
    2241              : 
    2242          160 :     fn rule(t: &str, attrs: &[&str]) -> Rule {
    2243              :         Rule {
    2244          160 :             types: vec![iri(t)],
    2245          160 :             attributes: attrs.iter().map(|a| iri(a)).collect(),
    2246          160 :             ..Rule::default()
    2247              :         }
    2248          160 :     }
    2249              : 
    2250              :     /// `{Car in zone A: speed, location, status}`, `{Car: status}`,
    2251              :     /// `{User: name}`, in the internal form `repr::type_rules` produces.
    2252           20 :     fn zones() -> Filter {
    2253           20 :         Filter {
    2254           20 :             rules: vec![
    2255           20 :                 Rule {
    2256           20 :                     geo_q: Some(zone_a()),
    2257           20 :                     attributes: vec![
    2258           20 :                         iri("speed"),
    2259           20 :                         antares_ql::geo::LOCATION_IRI.to_owned(),
    2260           20 :                         iri("status"),
    2261           20 :                     ],
    2262           20 :                     ..rule("Car", &[])
    2263           20 :                 },
    2264           20 :                 rule("Car", &["status"]),
    2265           20 :                 rule("User", &["name"]),
    2266           20 :             ],
    2267           20 :             ..Filter::default()
    2268           20 :         }
    2269           20 :     }
    2270              : 
    2271           60 :     fn car(lon: f64) -> Value {
    2272           60 :         json!({"id": "urn:ngsi-ld:Car:1", "type": [iri("Car")],
    2273           60 :                (iri("speed")): [{"type": "Property", "value": 80}],
    2274           60 :                (iri("status")): [{"type": "Property", "value": "ok"}],
    2275           60 :                "https://uri.etsi.org/ngsi-ld/location": [{"type": "GeoProperty",
    2276           60 :                    "value": {"type": "Point", "coordinates": [lon, 0.5]}}]})
    2277           60 :     }
    2278              : 
    2279           28 :     fn ctx() -> antares_jsonld::Context {
    2280           28 :         antares_jsonld::core_context()
    2281           28 :     }
    2282              : 
    2283              :     /// A rule that narrows ids selects by id, and a document with no
    2284              :     /// id only when it narrows none.
    2285              :     #[test]
    2286            4 :     fn a_rule_selects_by_type_and_id() {
    2287            4 :         let any = rule("Car", &["speed"]);
    2288            4 :         let one = Rule {
    2289            4 :             ids: vec!["urn:ngsi-ld:Car:1".into()],
    2290            4 :             ..rule("Car", &["speed"])
    2291            4 :         };
    2292            4 :         let doc = car(0.5);
    2293            4 :         let anonymous = json!({"type": [iri("Car")]});
    2294            4 :         assert!(any.selects(&doc) && one.selects(&doc));
    2295            4 :         assert!(any.selects(&anonymous), "no id narrowing, no id needed");
    2296            4 :         assert!(!one.selects(&anonymous), "an id narrowing needs an id");
    2297            4 :         assert!(!one.selects(&json!({"id": "urn:ngsi-ld:Car:2", "type": [iri("Car")]})));
    2298            4 :         assert!(!one.selects(&json!({"id": "urn:ngsi-ld:Car:1", "type": [iri("Bus")]})));
    2299            4 :     }
    2300              : 
    2301              :     /// the union over the rules that hold, nothing for an
    2302              :     /// Entity none holds for, and an omit on any type wins.
    2303              :     #[test]
    2304            4 :     fn shown_is_the_union_of_the_rules_that_hold_and_any_omit_wins() {
    2305            4 :         let f = zones();
    2306            4 :         let ctx = ctx();
    2307            4 :         let inside = f.shown(&car(0.5), &ctx).expect("in view");
    2308            4 :         let outside = f.shown(&car(5.0), &ctx).expect("in view");
    2309            4 :         assert!(inside.allows(&iri("speed")) && inside.allows(&iri("status")));
    2310            4 :         assert!(!outside.allows(&iri("speed")), "speed only in zone A");
    2311            4 :         assert!(outside.allows(&iri("status")));
    2312            4 :         let robot = json!({"id": "urn:ngsi-ld:Robot:1", "type": [iri("Robot")]});
    2313            4 :         assert_eq!(f.shown(&robot, &ctx), None, "no rule holds: out");
    2314            4 :         let both = json!({"id": "urn:x", "type": [iri("Car"), iri("User")],
    2315            4 :                           (iri("name")): [{"type": "Property", "value": "n"}]});
    2316            4 :         let shown = f.shown(&both, &ctx).expect("in view");
    2317            4 :         assert!(
    2318            4 :             shown.allows(&iri("name")) && shown.allows(&iri("status")),
    2319              :             "union"
    2320              :         );
    2321            4 :         let omitting = Filter {
    2322            4 :             omit_by_type: BTreeMap::from([(iri("User"), vec![iri("status")])]),
    2323            4 :             ..zones()
    2324            4 :         };
    2325            4 :         let shown = omitting.shown(&both, &ctx).expect("in view");
    2326            4 :         assert!(!shown.allows(&iri("status")), "an omit on any type wins");
    2327            4 :         let only_omit = Filter {
    2328            4 :             omit_by_type: omitting.omit_by_type.clone(),
    2329            4 :             ..Filter::default()
    2330            4 :         };
    2331            4 :         let shown = only_omit.shown(&robot, &ctx).expect("no rules: in");
    2332            4 :         assert!(shown.allows(&iri("serial")), "no rules: omit alone");
    2333            4 :     }
    2334              : 
    2335              :     /// A rule whose `q` follows a link never holds (fail closed), and a
    2336              :     /// rule naming no type holds for nothing.
    2337              :     #[test]
    2338            4 :     fn a_rule_reading_a_link_or_naming_no_type_never_holds() {
    2339            4 :         let ctx = ctx();
    2340            4 :         let linked = Rule {
    2341            4 :             q: Some(Box::new(
    2342            4 :                 antares_ql::parse_q("ownedBy{name}==\"x\"").expect("q"),
    2343            4 :             )),
    2344            4 :             ..rule("Car", &["speed"])
    2345            4 :         };
    2346            4 :         assert!(linked.q.as_deref().is_some_and(QNode::has_linked_paths));
    2347            4 :         assert!(!linked.holds(&car(0.5), &ctx));
    2348            4 :         let negated = Rule {
    2349            4 :             q: Some(Box::new(antares_ql::parse_q("!ownedBy{name}").expect("q"))),
    2350            4 :             ..rule("Car", &["speed"])
    2351            4 :         };
    2352            4 :         assert!(!negated.holds(&car(0.5), &ctx), "not even negated");
    2353            4 :         assert!(!Rule::default().holds(&car(0.5), &ctx));
    2354            4 :         let local = Rule {
    2355            4 :             q: Some(Box::new(antares_ql::parse_q("speed>50").expect("q"))),
    2356            4 :             ..rule("Car", &["status"])
    2357            4 :         };
    2358            4 :         assert!(local.holds(&car(0.5), &ctx));
    2359            4 :     }
    2360              : 
    2361              :     /// the narrowing rules per rule (K22): a write names only Attributes the
    2362              :     /// rules that hold for its post-image show.
    2363              :     #[test]
    2364            4 :     fn write_that_adds_speed_outside_zone_a_is_refused() {
    2365            4 :         let f = zones();
    2366            4 :         let ctx = ctx();
    2367            4 :         let speed = iri("speed");
    2368            4 :         let status = iri("status");
    2369            4 :         assert!(f.admits_write(&car(0.5), &[&speed, &status], &ctx));
    2370            4 :         assert!(
    2371            4 :             !f.admits_write(&car(5.0), &[&speed], &ctx),
    2372              :             "speed outside A"
    2373              :         );
    2374            4 :         assert!(f.admits_write(&car(5.0), &[&status], &ctx));
    2375            4 :         let name = iri("name");
    2376            4 :         assert!(
    2377            4 :             !f.admits_write(&car(0.5), &[&speed, &name], &ctx),
    2378              :             "no one rule shows both"
    2379              :         );
    2380            4 :         let robot = json!({"id": "urn:ngsi-ld:Robot:1", "type": [iri("Robot")]});
    2381            4 :         assert!(!f.admits_write(&robot, &[], &ctx), "outside every rule");
    2382            4 :     }
    2383              : 
    2384              :     /// A type's list split over two rules of the same types (the Portal
    2385              :     /// splits past 256 names) is written whole: a write naming one name of
    2386              :     /// each part is admitted, and a name neither part shows is still refused
    2387              :     /// (17,025 open-data rows refused once).
    2388              :     #[test]
    2389            4 :     fn a_write_across_a_split_list_is_admitted_and_an_unlisted_name_refused() {
    2390            4 :         let ctx = ctx();
    2391            4 :         let f = Filter {
    2392            4 :             rules: vec![rule("Car", &["speed"]), rule("Car", &["status"])],
    2393            4 :             ..Filter::default()
    2394            4 :         };
    2395            4 :         let (speed, status, name) = (iri("speed"), iri("status"), iri("name"));
    2396            4 :         assert!(f.admits_write(&car(5.0), &[&speed, &status], &ctx));
    2397            4 :         assert!(!f.admits_write(&car(5.0), &[&speed, &name], &ctx));
    2398            4 :         let user = json!({"id": "urn:ngsi-ld:User:1", "type": [iri("User")]});
    2399            4 :         assert!(!f.admits_write(&user, &[&speed], &ctx), "no rule holds");
    2400            4 :     }
    2401              : 
    2402              :     /// A rule over every Attribute admits a write naming any of them, and
    2403              :     /// discloses nothing it reads.
    2404              :     #[test]
    2405            4 :     fn a_rule_over_every_attribute_admits_any_written_name() {
    2406            4 :         let ctx = ctx();
    2407            4 :         let whole = Rule {
    2408            4 :             every_attribute: true,
    2409            4 :             q: Some(Box::new(antares_ql::parse_q("speed>50").expect("q"))),
    2410            4 :             ..rule("Car", &[])
    2411            4 :         };
    2412            4 :         let f = Filter {
    2413            4 :             rules: vec![whole.clone()],
    2414            4 :             ..Filter::default()
    2415            4 :         };
    2416            4 :         let (speed, name) = (iri("speed"), iri("name"));
    2417            4 :         assert!(f.admits_write(&car(0.5), &[&speed, &name], &ctx));
    2418            4 :         let narrow = Filter {
    2419            4 :             rules: vec![rule("Car", &[])],
    2420            4 :             ..Filter::default()
    2421            4 :         };
    2422            4 :         assert!(!narrow.admits_write(&car(0.5), &[&speed], &ctx));
    2423            4 :         assert!(whole.discloses(&ctx).is_empty());
    2424            4 :     }
    2425              : 
    2426              :     /// The loader's disclosure report: zone A's rule reads `location` and
    2427              :     /// shows it, so it discloses nothing; a rule on `speed>100` showing only
    2428              :     /// `status` discloses `speed`.
    2429              :     #[test]
    2430            4 :     fn discloses_names_what_the_condition_reads_and_the_rule_does_not_show() {
    2431            4 :         let ctx = ctx();
    2432            4 :         assert!(zones().rules[0].discloses(&ctx).is_empty());
    2433            4 :         let fast = Rule {
    2434            4 :             q: Some(Box::new(antares_ql::parse_q("speed>100").expect("q"))),
    2435            4 :             geo_q: Some(zone_a()),
    2436            4 :             ..rule("Car", &["status"])
    2437            4 :         };
    2438            4 :         assert_eq!(
    2439            4 :             fast.discloses(&ctx),
    2440            4 :             vec![
    2441            4 :                 iri("speed"),
    2442            4 :                 "https://uri.etsi.org/ngsi-ld/location".to_owned()
    2443              :             ]
    2444              :         );
    2445            4 :     }
    2446              : 
    2447              :     #[test]
    2448            4 :     fn project_keeps_the_union_and_the_frame() {
    2449            4 :         let f = Filter {
    2450            4 :             rules: vec![
    2451            4 :                 Rule {
    2452            4 :                     types: vec!["Car".to_owned()],
    2453            4 :                     attributes: vec!["speed".to_owned(), "email".to_owned()],
    2454            4 :                     ..Rule::default()
    2455            4 :                 },
    2456            4 :                 Rule {
    2457            4 :                     types: vec!["User".to_owned()],
    2458            4 :                     attributes: vec!["name".to_owned()],
    2459            4 :                     ..Rule::default()
    2460            4 :                 },
    2461            4 :             ],
    2462            4 :             omit_by_type: BTreeMap::from([("User".to_owned(), vec!["email".to_owned()])]),
    2463            4 :             ..Filter::default()
    2464            4 :         };
    2465            4 :         let mut doc = json!({"id": "urn:x", "type": ["Car", "User"], "@context": "c",
    2466            4 :                              "speed": 1, "name": 2, "email": 3, "serial": 4});
    2467            4 :         f.project(&mut doc);
    2468            4 :         assert_eq!(
    2469              :             doc,
    2470            4 :             json!({"id": "urn:x", "type": ["Car", "User"], "@context": "c",
    2471            4 :                    "speed": 1, "name": 2})
    2472              :         );
    2473            4 :     }
    2474              :     use serde_json::json;
    2475              : 
    2476           28 :     fn subject() -> Subject {
    2477           28 :         Subject {
    2478           28 :             tenant: antares_model::TenantId::default(),
    2479           28 :             headers: vec![("X-Subject".into(), "a-token-shaped-string".into())],
    2480           28 :         }
    2481           28 :     }
    2482              : 
    2483              :     /// Run the contract and report whether it refused the engine.
    2484            8 :     async fn contract_holds(engine: &dyn PolicyEngine) -> bool {
    2485            8 :         let hook = std::panic::take_hook();
    2486            8 :         std::panic::set_hook(Box::new(|_| {}));
    2487            8 :         let outcome = std::panic::AssertUnwindSafe(run_policy_contract(engine))
    2488            8 :             .catch_unwind()
    2489            8 :             .await;
    2490            8 :         std::panic::set_hook(hook);
    2491            8 :         outcome.is_ok()
    2492            8 :     }
    2493              : 
    2494              :     #[tokio::test]
    2495            4 :     async fn the_built_in_engine_holds_the_contract() {
    2496            4 :         run_policy_contract(&AllowAll).await;
    2497            4 :     }
    2498              : 
    2499              :     /// An engine that narrows is what the seam is for, and the contract
    2500              :     /// must not stand in its way.
    2501              :     struct Narrowing;
    2502              : 
    2503              :     impl PolicyEngine for Narrowing {
    2504            4 :         fn name(&self) -> &str {
    2505            4 :             "narrowing"
    2506            4 :         }
    2507           44 :         fn decide<'a>(&'a self, _s: &'a Subject, _o: &'a Operation<'a>) -> DecisionFuture<'a> {
    2508           44 :             Box::pin(std::future::ready(Decision::Filter(Filter {
    2509           44 :                 omit: vec!["brand".into()],
    2510           44 :                 restricted: true,
    2511           44 :                 ..Filter::default()
    2512           44 :             })))
    2513           44 :         }
    2514            4 :         fn pre_notify(&self, _s: &Subject, _sub: &Value, n: &mut Value) -> NotifyDecision {
    2515            4 :             if let Some(data) = n.get_mut("data").and_then(Value::as_array_mut) {
    2516            4 :                 for entity in data {
    2517            4 :                     if let Some(o) = entity.as_object_mut() {
    2518            4 :                         o.remove("brand");
    2519            4 :                     }
    2520              :                 }
    2521            0 :             }
    2522            4 :             NotifyDecision::Filter(Filter {
    2523            4 :                 omit: vec!["brand".into()],
    2524            4 :                 ..Filter::default()
    2525            4 :             })
    2526            4 :         }
    2527              :     }
    2528              : 
    2529              :     #[tokio::test]
    2530            4 :     async fn an_engine_that_only_narrows_holds_the_contract() {
    2531            4 :         assert!(contract_holds(&Narrowing).await);
    2532            4 :     }
    2533              : 
    2534              :     /// The contract has teeth or it proves nothing: this engine puts a
    2535              :     /// member into a notification nobody asked for.
    2536              :     struct Widening;
    2537              : 
    2538              :     impl PolicyEngine for Widening {
    2539            4 :         fn name(&self) -> &str {
    2540            4 :             "widening"
    2541            4 :         }
    2542           44 :         fn decide<'a>(&'a self, _s: &'a Subject, _o: &'a Operation<'a>) -> DecisionFuture<'a> {
    2543           44 :             Box::pin(std::future::ready(Decision::Allow))
    2544           44 :         }
    2545            4 :         fn pre_notify(&self, _s: &Subject, _sub: &Value, n: &mut Value) -> NotifyDecision {
    2546            4 :             if let Some(o) = n.as_object_mut() {
    2547            4 :                 o.insert("stowaway".into(), json!(true));
    2548            4 :             }
    2549            4 :             NotifyDecision::Deliver
    2550            4 :         }
    2551              :     }
    2552              : 
    2553              :     #[tokio::test]
    2554            4 :     async fn the_contract_refuses_an_engine_that_widens_a_notification() {
    2555            4 :         assert!(
    2556            4 :             !contract_holds(&Widening).await,
    2557            4 :             "the contract passed an engine that added a member to a notification"
    2558            4 :         );
    2559            4 :     }
    2560              : 
    2561              :     #[test]
    2562            4 :     fn a_pick_keeps_the_frame_and_drops_the_rest() {
    2563            4 :         let mut doc = json!({"id": "urn:ngsi-ld:Vehicle:1", "type": "Vehicle",
    2564            4 :                              "speed": 1, "brand": "Skoda"});
    2565            4 :         Filter {
    2566            4 :             pick: vec!["speed".into()],
    2567            4 :             ..Filter::default()
    2568            4 :         }
    2569            4 :         .project(&mut doc);
    2570            4 :         assert_eq!(
    2571              :             doc,
    2572            4 :             json!({"id": "urn:ngsi-ld:Vehicle:1", "type": "Vehicle", "speed": 1})
    2573              :         );
    2574            4 :     }
    2575              : 
    2576              :     #[test]
    2577            4 :     fn an_omit_cannot_remove_what_makes_it_an_entity() {
    2578            4 :         let mut doc = json!({"id": "urn:ngsi-ld:Vehicle:1", "type": "Vehicle", "speed": 1});
    2579            4 :         Filter {
    2580            4 :             omit: vec!["id".into(), "type".into(), "speed".into()],
    2581            4 :             ..Filter::default()
    2582            4 :         }
    2583            4 :         .project(&mut doc);
    2584            4 :         assert_eq!(
    2585              :             doc,
    2586            4 :             json!({"id": "urn:ngsi-ld:Vehicle:1", "type": "Vehicle"})
    2587              :         );
    2588            4 :     }
    2589              : 
    2590              :     #[test]
    2591            4 :     fn a_pick_of_something_absent_adds_nothing() {
    2592            4 :         let mut doc = json!({"id": "urn:ngsi-ld:Vehicle:1", "type": "Vehicle", "speed": 1});
    2593            4 :         Filter {
    2594            4 :             pick: vec!["mileage".into()],
    2595            4 :             ..Filter::default()
    2596            4 :         }
    2597            4 :         .project(&mut doc);
    2598            4 :         assert_eq!(
    2599              :             doc,
    2600            4 :             json!({"id": "urn:ngsi-ld:Vehicle:1", "type": "Vehicle"})
    2601              :         );
    2602            4 :     }
    2603              : 
    2604              :     #[test]
    2605            4 :     fn a_projection_leaves_a_non_object_alone() {
    2606            4 :         let mut doc = json!(["not", "an", "object"]);
    2607            4 :         Filter {
    2608            4 :             pick: vec!["speed".into()],
    2609            4 :             omit: vec!["brand".into()],
    2610            4 :             ..Filter::default()
    2611            4 :         }
    2612            4 :         .project(&mut doc);
    2613            4 :         assert_eq!(doc, json!(["not", "an", "object"]));
    2614            4 :     }
    2615              : 
    2616              :     #[test]
    2617            4 :     fn narrowing_a_whole_tenant_operation_is_a_refusal() {
    2618            4 :         let narrowed = Decision::Filter(Filter {
    2619            4 :             omit: vec!["speed".into()],
    2620            4 :             ..Filter::default()
    2621            4 :         });
    2622           16 :         for clause in WHOLE_TENANT {
    2623           16 :             assert!(matches!(
    2624           16 :                 resolve(clause, narrowed.clone()),
    2625              :                 Decision::Deny(_)
    2626              :             ));
    2627              :         }
    2628              :         // and every other clause outside FILTERABLE, because the handler
    2629              :         // there never reads the Filter
    2630           24 :         for clause in ["5.6.1", "5.6.6", "5.8.1", "5.9.2", "5.13.2", "5.8.4"] {
    2631           24 :             assert!(
    2632           24 :                 matches!(resolve(clause, narrowed.clone()), Decision::Deny(_)),
    2633              :                 "{clause} dropped a narrowing instead of refusing it"
    2634              :             );
    2635              :         }
    2636            4 :     }
    2637              : 
    2638              :     /// The reads that do read it keep it: the seam refuses what would be
    2639              :     /// dropped, and nothing more.
    2640              :     #[test]
    2641            4 :     fn a_filterable_read_keeps_its_narrowing() {
    2642            4 :         let narrowed = Decision::Filter(Filter {
    2643            4 :             omit: vec!["speed".into()],
    2644            4 :             ..Filter::default()
    2645            4 :         });
    2646            4 :         let selected = Decision::Filter(Filter {
    2647            4 :             scope_q: Some("/mine".into()),
    2648            4 :             ..Filter::default()
    2649            4 :         });
    2650           60 :         for clause in FILTERABLE {
    2651           60 :             assert_eq!(resolve(clause, selected.clone()), selected, "{clause}");
    2652           60 :             if RECORD_LISTS.contains(&clause) {
    2653              :                 // a record list selects and never projects
    2654           12 :                 assert!(matches!(
    2655           12 :                     resolve(clause, narrowed.clone()),
    2656              :                     Decision::Deny(_)
    2657              :                 ));
    2658              :             } else {
    2659           48 :                 assert_eq!(resolve(clause, narrowed.clone()), narrowed, "{clause}");
    2660              :             }
    2661              :         }
    2662            4 :     }
    2663              : 
    2664              :     #[test]
    2665            4 :     fn an_empty_filter_on_a_whole_tenant_operation_is_not_a_refusal() {
    2666            4 :         assert_eq!(
    2667            4 :             resolve("5.6.21", Decision::Filter(Filter::default())),
    2668              :             Decision::Allow
    2669              :         );
    2670            4 :     }
    2671              : 
    2672              :     struct Panicking;
    2673              : 
    2674              :     impl PolicyEngine for Panicking {
    2675            0 :         fn name(&self) -> &str {
    2676            0 :             "panicking"
    2677            0 :         }
    2678            4 :         fn decide<'a>(&'a self, _s: &'a Subject, _o: &'a Operation<'a>) -> DecisionFuture<'a> {
    2679            4 :             Box::pin(async { panic!("the engine is broken") })
    2680            4 :         }
    2681            4 :         fn pre_notify(&self, _s: &Subject, _sub: &Value, _n: &mut Value) -> NotifyDecision {
    2682            4 :             panic!("the engine is broken")
    2683              :         }
    2684              :     }
    2685              : 
    2686              :     /// The other half of the same failure: a synchronous engine decides in
    2687              :     /// the call that BUILDS the future, so a guard around the future alone
    2688              :     /// never sees it. The reference engine has exactly this shape.
    2689              :     struct PanickingEagerly;
    2690              : 
    2691              :     impl PolicyEngine for PanickingEagerly {
    2692            0 :         fn name(&self) -> &str {
    2693            0 :             "panicking-eagerly"
    2694            0 :         }
    2695            4 :         fn decide<'a>(&'a self, _s: &'a Subject, _o: &'a Operation<'a>) -> DecisionFuture<'a> {
    2696            4 :             panic!("the engine is broken")
    2697              :         }
    2698            0 :         fn pre_notify(&self, _s: &Subject, _sub: &Value, _n: &mut Value) -> NotifyDecision {
    2699            0 :             NotifyDecision::Deliver
    2700            0 :         }
    2701              :     }
    2702              : 
    2703              :     #[tokio::test]
    2704            4 :     async fn an_engine_that_panics_before_it_returns_a_future_denies_too() {
    2705            4 :         let hook = std::panic::take_hook();
    2706            5 :         std::panic::set_hook(Box::new(|_| {}));
    2707            4 :         let decided = decide(&PanickingEagerly, &subject(), &Operation::new("5.6.1")).await;
    2708            4 :         std::panic::set_hook(hook);
    2709            4 :         assert_eq!(decided, Decision::Deny(ENGINE_FAILED.to_owned()));
    2710            4 :     }
    2711              : 
    2712              :     #[tokio::test]
    2713            4 :     async fn an_engine_that_panics_denies_rather_than_allows() {
    2714            4 :         let hook = std::panic::take_hook();
    2715            7 :         std::panic::set_hook(Box::new(|_| {}));
    2716            4 :         let decided = decide(&Panicking, &subject(), &Operation::new("5.6.1")).await;
    2717            4 :         let mut notification = json!({"type": "Notification"});
    2718            4 :         let notified = pre_notify(&Panicking, &subject(), &json!({}), &mut notification);
    2719            4 :         std::panic::set_hook(hook);
    2720            4 :         assert_eq!(decided, Decision::Deny(ENGINE_FAILED.to_owned()));
    2721            4 :         assert_eq!(notified, NotifyDecision::Drop);
    2722            4 :     }
    2723              : 
    2724              :     /// Real time, not a paused clock: the workspace tokio is built without
    2725              :     /// `test-util`, so the test pays the timeout it asserts.
    2726              :     #[tokio::test]
    2727            4 :     async fn an_engine_that_never_answers_denies() {
    2728              :         struct Never;
    2729            4 :         impl PolicyEngine for Never {
    2730            4 :             fn name(&self) -> &str {
    2731            4 :                 "never"
    2732            4 :             }
    2733            4 :             fn decide<'a>(&'a self, _s: &'a Subject, _o: &'a Operation<'a>) -> DecisionFuture<'a> {
    2734            4 :                 Box::pin(std::future::pending())
    2735            4 :             }
    2736            4 :             fn pre_notify(&self, _s: &Subject, _sub: &Value, _n: &mut Value) -> NotifyDecision {
    2737            4 :                 NotifyDecision::Deliver
    2738            4 :             }
    2739            4 :         }
    2740            4 :         assert_eq!(
    2741            4 :             decide(&Never, &subject(), &Operation::new("5.6.1")).await,
    2742            4 :             Decision::Deny(ENGINE_TIMED_OUT.to_owned())
    2743            4 :         );
    2744            4 :     }
    2745              : 
    2746              :     #[test]
    2747            4 :     fn the_subject_never_prints_its_header_values() {
    2748            4 :         let printed = format!("{:?}", subject());
    2749            4 :         assert!(printed.contains("X-Subject"), "{printed}");
    2750            4 :         assert!(
    2751            4 :             !printed.contains("a-token-shaped-string"),
    2752              :             "a header value reached a log line: {printed}"
    2753              :         );
    2754            4 :     }
    2755              : 
    2756              :     #[test]
    2757            4 :     fn an_operation_never_prints_the_payload() {
    2758            4 :         let body = json!({"id": "urn:ngsi-ld:Vehicle:1", "plate": "BB-123-XY"});
    2759            4 :         let printed = format!(
    2760              :             "{:?}",
    2761            4 :             Operation {
    2762            4 :                 body: Some(&body),
    2763            4 :                 ..Operation::new("5.6.1")
    2764            4 :             }
    2765              :         );
    2766            4 :         assert!(!printed.contains("BB-123-XY"), "{printed}");
    2767            4 :     }
    2768              : 
    2769              :     /// the policy survivors of a mutants run.
    2770              :     #[test]
    2771            4 :     fn the_timeout_takes_a_positive_integer_or_falls_back() {
    2772            4 :         assert_eq!(timeout_ms(None), 250);
    2773            4 :         assert_eq!(timeout_ms(Some("0")), 250, "zero would deny every request");
    2774            4 :         assert_eq!(timeout_ms(Some("x")), 250);
    2775            4 :         assert_eq!(timeout_ms(Some("1")), 1);
    2776            4 :         assert_eq!(timeout_ms(Some("300")), 300);
    2777            4 :     }
    2778              : 
    2779              :     #[test]
    2780            4 :     fn a_single_string_counts_as_a_one_element_list() {
    2781            4 :         let doc = json!({"type": "T", "scope": "/a"});
    2782            4 :         assert_eq!(entity_types(&doc), ["T"]);
    2783            4 :         assert_eq!(StoredImage::of(&doc).scope, ["/a"]);
    2784            4 :         let listed = json!({"type": ["T", "U"], "scope": ["/a", "/b"]});
    2785            4 :         assert_eq!(entity_types(&listed), ["T", "U"]);
    2786            4 :         assert_eq!(StoredImage::of(&listed).scope, ["/a", "/b"]);
    2787            4 :     }
    2788              : 
    2789              :     #[test]
    2790            4 :     fn an_operation_logs_its_shape_and_never_its_payload() {
    2791            4 :         let op = Operation::new("5.6.1");
    2792            4 :         let shown = format!("{op:?}");
    2793            4 :         assert!(shown.starts_with("Operation {"), "{shown}");
    2794            4 :         assert!(shown.contains("clause: \"5.6.1\""), "{shown}");
    2795            4 :         assert!(shown.contains("ids: 0"), "{shown}");
    2796            4 :     }
    2797              : 
    2798              :     #[test]
    2799            4 :     fn a_rule_is_conditional_on_any_one_of_its_conditions() {
    2800            4 :         let plain = rule("Car", &["speed"]);
    2801            4 :         assert!(!plain.is_conditional());
    2802            4 :         let q = Rule {
    2803            4 :             q: Some(Box::new(antares_ql::parse_q("speed>1").expect("q"))),
    2804            4 :             ..plain.clone()
    2805            4 :         };
    2806            4 :         let scope = Rule {
    2807            4 :             scope_q: Some("/a".to_owned()),
    2808            4 :             ..plain.clone()
    2809            4 :         };
    2810            4 :         assert!(q.is_conditional() && scope.is_conditional());
    2811            4 :     }
    2812              : 
    2813              :     #[test]
    2814            4 :     fn shown_nothing_admits_no_attribute() {
    2815            4 :         let nothing = Shown::nothing();
    2816            4 :         assert_eq!(nothing.only, Some(Default::default()));
    2817            4 :         assert!(!nothing.allows(&iri("name")));
    2818            4 :     }
    2819              : 
    2820              :     #[test]
    2821            4 :     fn a_write_is_refused_only_for_the_attribute_its_type_omits() {
    2822            4 :         let ctx = ctx();
    2823            4 :         let f = Filter {
    2824            4 :             omit_by_type: BTreeMap::from([(iri("User"), vec![iri("status")])]),
    2825            4 :             ..Filter::default()
    2826            4 :         };
    2827            4 :         let user = json!({"id": "urn:x", "type": [iri("User")]});
    2828            4 :         assert!(f.admits_write(&user, &[iri("name").as_str()], &ctx));
    2829            4 :         assert!(!f.admits_write(&user, &[iri("status").as_str()], &ctx));
    2830            4 :     }
    2831              : 
    2832              :     /// The image of a stored Entity names
    2833              :     /// its Attributes, never what frames, dates or annotates it.
    2834              :     #[test]
    2835            4 :     fn a_stored_image_names_attributes_only() {
    2836            4 :         let doc = json!({"id": "urn:ngsi-ld:Car:1", "type": [iri("Car")], "scope": ["/a"],
    2837            4 :                          "createdAt": "t", "modifiedAt": "t", "deletedAt": "t",
    2838            4 :                          "@context": [], "__tenant": "acme",
    2839            4 :                          (iri("speed")): [{"type": "Property", "value": 1}]});
    2840            4 :         let image = StoredImage::of(&doc);
    2841            4 :         assert_eq!(image.types, vec![iri("Car")]);
    2842            4 :         assert_eq!(image.scope, vec!["/a".to_owned()]);
    2843            4 :         assert_eq!(image.attribute_names, vec![iri("speed")]);
    2844            4 :     }
    2845              : 
    2846              :     #[test]
    2847            4 :     fn mapping_the_names_maps_both_sets() {
    2848            4 :         let shown = Shown {
    2849            4 :             only: Some(std::collections::BTreeSet::from([Cow::Borrowed("a")])),
    2850            4 :             omitted: std::collections::BTreeSet::from([Cow::Borrowed("b")]),
    2851            4 :         };
    2852            4 :         let mapped = shown.map_names(str::to_uppercase);
    2853            4 :         assert_eq!(
    2854              :             mapped.only,
    2855            4 :             Some(std::collections::BTreeSet::from([Cow::Borrowed("A")]))
    2856              :         );
    2857            4 :         assert_eq!(
    2858              :             mapped.omitted,
    2859            4 :             std::collections::BTreeSet::from([Cow::Borrowed("B")])
    2860              :         );
    2861            4 :     }
    2862              : 
    2863              :     /// a notification's shown set outlives its rules unchanged; an
    2864              :     /// empty set would show the whole Entity.
    2865              :     #[test]
    2866            4 :     fn an_owned_shown_set_keeps_both_sets() {
    2867            4 :         let shown = Shown {
    2868            4 :             only: Some(std::collections::BTreeSet::from([Cow::Borrowed("a")])),
    2869            4 :             omitted: std::collections::BTreeSet::from([Cow::Borrowed("b")]),
    2870            4 :         };
    2871            4 :         let owned = shown.clone().into_owned();
    2872            4 :         assert_eq!(owned, shown);
    2873            4 :         assert!(owned.allows("a") && !owned.allows("b") && !owned.allows("c"));
    2874            4 :     }
    2875              : 
    2876              :     /// only a rule with no condition, ids or id patterns admits by
    2877              :     /// type alone, the one the store may page as a type selection.
    2878              :     #[test]
    2879            4 :     fn a_rule_admits_by_type_alone_only_without_condition_or_ids() {
    2880            4 :         let typed = Rule {
    2881            4 :             types: vec![iri("Car")],
    2882            4 :             ..Rule::default()
    2883            4 :         };
    2884            4 :         assert!(typed.admits_by_type_alone());
    2885           12 :         for narrowed in [
    2886            4 :             Rule {
    2887            4 :                 ids: vec!["urn:ngsi-ld:Car:1".into()],
    2888            4 :                 ..typed.clone()
    2889            4 :             },
    2890            4 :             Rule {
    2891            4 :                 id_patterns: vec!["urn:.*".into()],
    2892            4 :                 ..typed.clone()
    2893            4 :             },
    2894            4 :             Rule {
    2895            4 :                 scope_q: Some("/a/#".into()),
    2896            4 :                 ..typed.clone()
    2897            4 :             },
    2898            4 :         ] {
    2899           12 :             assert!(!narrowed.admits_by_type_alone(), "{narrowed:?}");
    2900              :         }
    2901            4 :     }
    2902              : 
    2903              :     /// the broker's `__` members of a stored Subscription or
    2904              :     /// Registration are no Attributes a condition reads.
    2905              :     #[test]
    2906            4 :     fn a_record_image_leaves_the_brokers_members_out() {
    2907            4 :         let image = record_image(&json!({"id": "urn:ngsi-ld:Subscription:1",
    2908            4 :             "type": "Subscription", "__tenant": "acme", "description": "d"}));
    2909            4 :         let keys: Vec<&String> = image
    2910            4 :             .as_object()
    2911            4 :             .into_iter()
    2912            4 :             .flatten()
    2913            4 :             .map(|(k, _)| k)
    2914            4 :             .collect();
    2915            4 :         assert_eq!(keys.len(), 3, "{image}");
    2916           12 :         assert!(keys.iter().all(|k| !k.contains("__")), "{image}");
    2917            4 :     }
    2918              : 
    2919              :     #[test]
    2920            4 :     fn a_filter_says_its_areas_its_rules_and_its_restriction() {
    2921            4 :         let both = Filter {
    2922            4 :             geo_q: Some(zone_a()),
    2923            4 :             geo_also: vec![*zone_a()],
    2924            4 :             ..Filter::default()
    2925            4 :         };
    2926            4 :         assert_eq!(both.areas().count(), 2);
    2927            4 :         let also = Filter {
    2928            4 :             geo_also: vec![*zone_a()],
    2929            4 :             ..Filter::default()
    2930            4 :         };
    2931            4 :         assert_eq!(also.areas().count(), 1);
    2932            4 :         assert!(!also.is_empty());
    2933              : 
    2934            4 :         assert!(!Filter::default().has_rules());
    2935            4 :         assert!(zones().has_rules(), "rules alone");
    2936            4 :         let omits = Filter {
    2937            4 :             omit_by_type: BTreeMap::from([(iri("User"), vec![iri("status")])]),
    2938            4 :             ..Filter::default()
    2939            4 :         };
    2940            4 :         assert!(omits.has_rules(), "per-type omits alone");
    2941              : 
    2942            4 :         let mut headers = HeaderMap::new();
    2943            4 :         Filter::default().mark("me", &mut headers);
    2944            4 :         assert!(headers.get(RESTRICTED_HEADER).is_none());
    2945            4 :         assert!(headers.get("NGSILD-Warning").is_none());
    2946            4 :         Filter {
    2947            4 :             restricted: true,
    2948            4 :             ..Filter::default()
    2949            4 :         }
    2950            4 :         .mark("me", &mut headers);
    2951            4 :         assert_eq!(
    2952            4 :             headers.get(RESTRICTED_HEADER).and_then(|v| v.to_str().ok()),
    2953              :             Some("true")
    2954              :         );
    2955            4 :         assert!(headers.get("NGSILD-Warning").is_none(), "only when set");
    2956            4 :     }
    2957              : 
    2958              :     /// The request's window clamped to the narrowing's, written back
    2959              :     /// into the parameters; nothing changes without windows, a missing or
    2960              :     /// invalid window stays refused, and no overlap is a window of nothing.
    2961              :     #[test]
    2962            4 :     fn a_temporal_window_is_clamped_into_the_parameters() {
    2963           44 :         let p = |pairs: &[(&str, &str)]| -> std::collections::HashMap<String, String> {
    2964           44 :             pairs
    2965           44 :                 .iter()
    2966           44 :                 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
    2967           44 :                 .collect()
    2968           44 :         };
    2969           12 :         let t = |s: &str| TimeBound::At(s.parse().unwrap());
    2970            4 :         let between = Filter {
    2971            4 :             temporal: Box::new([Window {
    2972            4 :                 from: Some(t("2026-01-01T01:00:00Z")),
    2973            4 :                 to: Some(t("2026-01-01T02:00:00Z")),
    2974            4 :             }]),
    2975            4 :             ..Filter::default()
    2976            4 :         };
    2977            4 :         let odd = p(&[("timerel", "sideways")]);
    2978            4 :         assert_eq!(
    2979            4 :             Filter::default().narrow_window(&odd, true).unwrap(),
    2980              :             odd,
    2981              :             "no windows, no reading"
    2982              :         );
    2983            4 :         assert!(
    2984            4 :             between.narrow_window(&p(&[]), true).is_err(),
    2985              :             "5.7.4 still demands a window"
    2986              :         );
    2987            4 :         assert!(between.narrow_window(&odd, false).is_err());
    2988              : 
    2989            4 :         let out = between.narrow_window(&p(&[]), false).unwrap();
    2990            4 :         assert_eq!(out["timerel"], "between");
    2991            4 :         assert_eq!(out["timeAt"], "2026-01-01T01:00:00.000Z");
    2992            4 :         assert_eq!(out["endTimeAt"], "2026-01-01T02:00:00.000Z");
    2993              : 
    2994            4 :         let out = between
    2995            4 :             .narrow_window(
    2996            4 :                 &p(&[("timerel", "after"), ("timeAt", "2026-01-01T01:30:00Z")]),
    2997              :                 true,
    2998              :             )
    2999            4 :             .unwrap();
    3000            4 :         assert_eq!(
    3001            4 :             (out["timerel"].as_str(), out["timeAt"].as_str()),
    3002              :             ("between", "2026-01-01T01:30:00Z")
    3003              :         );
    3004              : 
    3005            4 :         let out = between
    3006            4 :             .narrow_window(
    3007            4 :                 &p(&[("timerel", "before"), ("timeAt", "2025-01-01T00:00:00Z")]),
    3008              :                 true,
    3009              :             )
    3010            4 :             .unwrap();
    3011            4 :         assert_eq!(
    3012            4 :             out["timeAt"], out["endTimeAt"],
    3013              :             "no overlap: between an instant and itself"
    3014              :         );
    3015              : 
    3016            4 :         let open = Filter {
    3017            4 :             temporal: Box::new([Window {
    3018            4 :                 from: None,
    3019            4 :                 to: Some(t("2026-01-01T02:00:00Z")),
    3020            4 :             }]),
    3021            4 :             ..Filter::default()
    3022            4 :         };
    3023            4 :         let out = open
    3024            4 :             .narrow_window(
    3025            4 :                 &p(&[
    3026            4 :                     ("timerel", "between"),
    3027            4 :                     ("timeAt", "2025-01-01T00:00:00Z"),
    3028            4 :                     ("endTimeAt", "2027-01-01T00:00:00Z"),
    3029            4 :                 ]),
    3030              :                 true,
    3031              :             )
    3032            4 :             .unwrap();
    3033            4 :         assert_eq!(
    3034            4 :             (out["timeAt"].as_str(), out["endTimeAt"].as_str()),
    3035              :             ("2025-01-01T00:00:00Z", "2026-01-01T02:00:00.000Z")
    3036              :         );
    3037            4 :         let out = open.narrow_window(&p(&[]), false).unwrap();
    3038            4 :         assert_eq!(out["timerel"], "before");
    3039            4 :         assert!(!out.contains_key("endTimeAt"));
    3040            4 :         let empty = Filter {
    3041            4 :             temporal: Box::new([Window::default()]),
    3042            4 :             ..Filter::default()
    3043            4 :         };
    3044            4 :         assert_eq!(
    3045            4 :             empty.narrow_window(&p(&[]), false).unwrap(),
    3046            4 :             p(&[]),
    3047              :             "an unbounded window narrows nothing"
    3048              :         );
    3049              : 
    3050              :         // a relative start rounds up to the millisecond, an end down
    3051            4 :         let slide = Filter {
    3052            4 :             temporal: Box::new([Window {
    3053            4 :                 from: Some(TimeBound::Ago(chrono::Duration::nanoseconds(1))),
    3054            4 :                 to: Some(TimeBound::Ago(chrono::Duration::nanoseconds(1))),
    3055            4 :             }]),
    3056            4 :             ..Filter::default()
    3057            4 :         };
    3058            4 :         let out = slide.narrow_window(&p(&[]), false).unwrap();
    3059            4 :         assert!(antares_model::dt_key(&out["timeAt"]) >= antares_model::dt_key(&out["endTimeAt"]));
    3060              : 
    3061              :         // the windows are on observedAt: a request on another timeproperty
    3062              :         // is answered nothing, even one whose own window overlaps
    3063            4 :         let out = between
    3064            4 :             .narrow_window(
    3065            4 :                 &p(&[
    3066            4 :                     ("timerel", "after"),
    3067            4 :                     ("timeAt", "2026-01-01T01:30:00Z"),
    3068            4 :                     ("timeproperty", "modifiedAt"),
    3069            4 :                 ]),
    3070              :                 true,
    3071              :             )
    3072            4 :             .unwrap();
    3073            4 :         assert_eq!(out["timeAt"], out["endTimeAt"], "{out:?}");
    3074            4 :         assert_eq!(out["timeAt"], "2026-01-01T01:00:00.000Z", "{out:?}");
    3075            4 :     }
    3076              : 
    3077              :     /// A bound renders to the millisecond 4.11 takes: a start rounds up
    3078              :     /// (only ever by less than a millisecond), an end down, an exact
    3079              :     /// millisecond stays; a window is half-open on those renderings.
    3080              :     #[test]
    3081            4 :     fn a_bound_rounds_only_inward_and_a_window_is_half_open() {
    3082           40 :         let at = |ns: u32| {
    3083           40 :             TimeBound::At(chrono::DateTime::from_timestamp(1_767_225_600, ns).expect("an instant"))
    3084           40 :         };
    3085            4 :         let now = chrono::Utc::now();
    3086           16 :         for (ns, start, end) in [
    3087            4 :             (0, "00.000", "00.000"),
    3088            4 :             (500, "00.001", "00.000"),
    3089            4 :             (999_000, "00.001", "00.000"),
    3090            4 :             (1_000_500, "00.002", "00.001"),
    3091            4 :         ] {
    3092           16 :             assert_eq!(
    3093           16 :                 at(ns).render(now, true),
    3094           16 :                 format!("2026-01-01T00:00:{start}Z"),
    3095              :                 "start at {ns} ns"
    3096              :             );
    3097           16 :             assert_eq!(
    3098           16 :                 at(ns).render(now, false),
    3099           16 :                 format!("2026-01-01T00:00:{end}Z"),
    3100              :                 "end at {ns} ns"
    3101              :             );
    3102              :         }
    3103            4 :         let w = Window {
    3104            4 :             from: Some(at(0)),
    3105            4 :             to: Some(at(2_000_000)),
    3106            4 :         };
    3107            4 :         assert!(
    3108            4 :             !w.admits("2025-12-31T23:59:59.999Z", now),
    3109              :             "before the start"
    3110              :         );
    3111            4 :         assert!(
    3112            4 :             w.admits("2026-01-01T00:00:00.000Z", now),
    3113              :             "the start is inside"
    3114              :         );
    3115            4 :         assert!(w.admits("2026-01-01T00:00:00.001Z", now), "inside");
    3116            4 :         assert!(
    3117            4 :             !w.admits("2026-01-01T00:00:00.002Z", now),
    3118              :             "the end is outside"
    3119              :         );
    3120            4 :         assert!(!w.admits("2026-01-01T00:00:00.003Z", now), "after the end");
    3121            4 :     }
    3122              : 
    3123              :     /// an evolution is judged as the Entity it now is, per
    3124              :     /// Attribute and `datasetId`, whatever order its instances are stored in.
    3125              :     #[test]
    3126            4 :     fn the_latest_image_keeps_the_latest_instance_per_dataset() {
    3127           20 :         let i = |v: &str, at: &str, ds: Option<&str>| {
    3128           20 :             let mut i = serde_json::json!({"type": "Property", "value": v, "observedAt": at});
    3129           20 :             if let Some(ds) = ds {
    3130            8 :                 i["datasetId"] = ds.into();
    3131           12 :             }
    3132           20 :             i
    3133           20 :         };
    3134            4 :         let doc = serde_json::json!({
    3135            4 :             "id": "urn:ngsi-ld:V:1",
    3136            4 :             "type": ["V"],
    3137            4 :             "zone": [
    3138            4 :                 i("A", "2026-01-01T00:00:02Z", None),
    3139            4 :                 i("B", "2026-01-01T00:00:01Z", None),
    3140            4 :                 i("C", "2026-01-01T00:00:01Z", Some("urn:ds:1")),
    3141            4 :                 i("D", "2026-01-01T00:00:03Z", Some("urn:ds:1")),
    3142            4 :                 i("E", "2026-01-01T00:00:02Z", None),
    3143              :             ],
    3144            4 :             "scope": [
    3145            4 :                 {"type": "Property", "value": ["/old"], "observedAt": "2026-01-01T00:00:01Z"},
    3146            4 :                 {"type": "Property", "value": ["/new"], "observedAt": "2026-01-01T00:00:02Z"},
    3147              :             ],
    3148            4 :             "modifiedAt": "2026-01-01T00:00:03Z",
    3149              :         });
    3150            4 :         let img = latest_image(&doc);
    3151            4 :         let zone: Vec<&str> = img["zone"]
    3152            4 :             .as_array()
    3153            4 :             .into_iter()
    3154            4 :             .flatten()
    3155            8 :             .filter_map(|i| i["value"].as_str())
    3156            4 :             .collect();
    3157            4 :         assert_eq!(zone, ["E", "D"], "a tie goes to the later one");
    3158            4 :         assert_eq!(img["scope"], serde_json::json!(["/new"]));
    3159            4 :         assert_eq!(img["type"], doc["type"]);
    3160            4 :         assert_eq!(img["modifiedAt"], doc["modifiedAt"]);
    3161            4 :         let plain = serde_json::json!({"id": "urn:ngsi-ld:V:1", "scope": "/a"});
    3162            4 :         assert_eq!(latest_image(&plain), plain);
    3163            4 :     }
    3164              : 
    3165              :     /// The engine's warning travels as one `NGSILD-Warning` value per
    3166              :     /// text, and a text that could not be one header line is refused.
    3167              :     #[test]
    3168            4 :     fn a_condition_holds_only_on_a_write_by_id_and_only_as_a_selection() {
    3169            4 :         let zone = Filter {
    3170            4 :             q: Some(Box::new(antares_ql::parse_q("zone==\"A\"").expect("q"))),
    3171            4 :             ..Filter::default()
    3172            4 :         };
    3173            4 :         let conditioned = Decision::AllowIf(zone.clone());
    3174           64 :         for clause in WRITE_BY_ID {
    3175           64 :             assert_eq!(
    3176           64 :                 resolve(clause, conditioned.clone()),
    3177              :                 conditioned,
    3178              :                 "{clause}"
    3179              :             );
    3180           64 :             assert_eq!(
    3181           64 :                 resolve(clause, Decision::AllowIf(Filter::default())),
    3182              :                 Decision::Allow,
    3183              :                 "{clause}: an empty condition selects everything"
    3184              :             );
    3185              :         }
    3186           24 :         for clause in ["5.6.1", "5.6.7", "5.6.16", "5.6.21", "5.7.1", "5.8.1"] {
    3187           24 :             assert!(
    3188           24 :                 matches!(resolve(clause, conditioned.clone()), Decision::Deny(_)),
    3189              :                 "{clause}"
    3190              :             );
    3191              :         }
    3192           20 :         for projecting in [
    3193            4 :             Filter {
    3194            4 :                 pick: vec!["speed".into()],
    3195            4 :                 ..zone.clone()
    3196            4 :             },
    3197            4 :             Filter {
    3198            4 :                 omit: vec!["speed".into()],
    3199            4 :                 ..zone.clone()
    3200            4 :             },
    3201            4 :             Filter {
    3202            4 :                 temporal: Box::new([Window::default()]),
    3203            4 :                 ..zone.clone()
    3204            4 :             },
    3205            4 :             Filter {
    3206            4 :                 omit_by_type: BTreeMap::from([("Vehicle".into(), vec!["speed".into()])]),
    3207            4 :                 ..zone.clone()
    3208            4 :             },
    3209            4 :             Filter {
    3210            4 :                 rules: vec![Rule {
    3211            4 :                     types: vec!["Vehicle".into()],
    3212            4 :                     temporal: vec![Window::default()],
    3213            4 :                     ..Rule::default()
    3214            4 :                 }],
    3215            4 :                 ..Filter::default()
    3216            4 :             },
    3217            4 :         ] {
    3218           20 :             assert!(
    3219            0 :                 matches!(
    3220           20 :                     resolve("5.6.2", Decision::AllowIf(projecting.clone())),
    3221              :                     Decision::Deny(_)
    3222              :                 ),
    3223              :                 "{projecting:?}"
    3224              :             );
    3225              :         }
    3226              : 
    3227            4 :         let dc = "https://uri.etsi.org/ngsi-ld/default-context/";
    3228           28 :         let doc = |z: &str| {
    3229           28 :             serde_json::json!({"id": "urn:ngsi-ld:Vehicle:v", "type": [format!("{dc}Vehicle")],
    3230           28 :                                format!("{dc}zone"): [{"type": "Property", "value": z}]})
    3231           28 :         };
    3232            4 :         let c = Condition::of(&zone).expect("a condition");
    3233            4 :         assert!(c.holds(&doc("A")) && !c.holds(&doc("B")));
    3234            4 :         assert!(Condition::pre(Some(&c), "urn:ngsi-ld:Vehicle:v", &doc("A")).is_ok());
    3235            4 :         assert!(matches!(
    3236            4 :             Condition::pre(Some(&c), "urn:ngsi-ld:Vehicle:v", &doc("B")),
    3237            4 :             Err(antares_model::NgsiError::ResourceNotFound(m))
    3238            4 :                 if m == "entity urn:ngsi-ld:Vehicle:v not found"
    3239              :         ));
    3240            4 :         assert!(Condition::pre(None, "urn:ngsi-ld:Vehicle:v", &doc("B")).is_ok());
    3241            4 :         let linked = Filter {
    3242            4 :             q: Some(Box::new(
    3243            4 :                 antares_ql::parse_q("owner.zone==\"A\"").expect("q"),
    3244            4 :             )),
    3245            4 :             ..Filter::default()
    3246            4 :         };
    3247            4 :         assert!(
    3248            4 :             !Condition::of(&linked)
    3249            4 :                 .expect("a condition")
    3250            4 :                 .holds(&doc("A")),
    3251              :             "a linked path reads no other Entity inside the write, so it never holds"
    3252              :         );
    3253            4 :         let scoped = Filter {
    3254            4 :             scope_q: Some("/a".to_owned()),
    3255            4 :             ..Filter::default()
    3256            4 :         };
    3257            4 :         assert!(
    3258            4 :             Condition::of(&scoped).is_some(),
    3259              :             "a scopeQ alone selects: it is a condition"
    3260              :         );
    3261            4 :         let vehicles = Condition::of(&Filter {
    3262            4 :             rules: vec![Rule {
    3263            4 :                 types: vec!["Vehicle".into()],
    3264            4 :                 ..Rule::default()
    3265            4 :             }],
    3266            4 :             ..Filter::default()
    3267            4 :         })
    3268            4 :         .expect("a type rule alone selects: it is a condition");
    3269            4 :         let car = serde_json::json!({"id": "urn:ngsi-ld:Car:c", "type": [format!("{dc}Car")]});
    3270            4 :         assert!(vehicles.holds(&doc("B")) && !vehicles.holds(&car));
    3271            4 :         assert!(Condition::of(&Filter::default()).is_none());
    3272            4 :         assert!(Condition::unforwarded(Some(&c), false).is_ok());
    3273            4 :         assert!(Condition::unforwarded(None, true).is_ok());
    3274            4 :         assert_eq!(
    3275            4 :             Condition::unforwarded(Some(&c), true),
    3276            4 :             Err(Denied(CONDITION_NOT_FORWARDED.into()))
    3277              :         );
    3278            4 :     }
    3279              : 
    3280              :     #[test]
    3281            4 :     fn a_warning_is_one_header_line_or_refused() {
    3282            4 :         let left_out = Warning::new("types left out by the filter: Building").unwrap();
    3283            4 :         let mut headers = HeaderMap::new();
    3284            4 :         Filter {
    3285            4 :             warning: Some(left_out.clone()),
    3286            4 :             ..Filter::default()
    3287            4 :         }
    3288            4 :         .mark("me", &mut headers);
    3289            4 :         let sent: Vec<_> = headers
    3290            4 :             .get_all("NGSILD-Warning")
    3291            4 :             .iter()
    3292            4 :             .map(|v| v.to_str().unwrap())
    3293            4 :             .collect();
    3294            4 :         assert_eq!(sent, ["199 me \"types left out by the filter: Building\""]);
    3295              : 
    3296           28 :         for bad in [
    3297            4 :             "a\nb",
    3298            4 :             "a\rb",
    3299            4 :             "tab\there",
    3300            4 :             "say \"x\"",
    3301            4 :             "back\\slash",
    3302            4 :             "é",
    3303            4 :             "",
    3304            4 :         ] {
    3305           28 :             assert!(Warning::new(bad).is_err(), "{bad:?}");
    3306              :         }
    3307            4 :         assert!(Warning::new(&"x".repeat(Warning::MAX_LEN)).is_ok());
    3308            4 :         assert!(Warning::new(&"x".repeat(Warning::MAX_LEN + 1)).is_err());
    3309              : 
    3310            4 :         let other = Warning::new("other").unwrap();
    3311            4 :         let joined = Warning::join(Some(&left_out), Some(other.clone())).unwrap();
    3312            4 :         assert_eq!(joined.texts().len(), 2);
    3313            4 :         assert_eq!(
    3314            4 :             Warning::join(Some(&left_out), Some(left_out.clone())),
    3315            4 :             Some(left_out.clone())
    3316              :         );
    3317            4 :         assert_eq!(
    3318            4 :             Warning::join(None, Some(other.clone())),
    3319            4 :             Some(other.clone())
    3320              :         );
    3321            4 :         assert_eq!(Warning::join(Some(&other), None), Some(other));
    3322           40 :         let many = (0..10).fold(None, |acc: Option<Warning>, i| {
    3323           40 :             Warning::join(acc.as_ref(), Some(Warning::new(&format!("w{i}")).unwrap()))
    3324           40 :         });
    3325            4 :         assert_eq!(many.unwrap().texts().len(), Warning::MAX_TEXTS);
    3326            4 :     }
    3327              : 
    3328              :     #[test]
    3329            4 :     fn only_a_pick_or_an_omit_hides_a_term() {
    3330            4 :         let speed = iri("speed");
    3331            4 :         assert!(!Filter::default().hides("speed", &speed));
    3332            4 :         let pick = Filter {
    3333            4 :             pick: vec!["status".into()],
    3334            4 :             ..Filter::default()
    3335            4 :         };
    3336            4 :         assert!(pick.hides("speed", &speed));
    3337            4 :         assert!(!pick.hides("status", &iri("status")));
    3338            4 :         let omit = Filter {
    3339            4 :             omit: vec!["speed".into()],
    3340            4 :             ..Filter::default()
    3341            4 :         };
    3342            4 :         assert!(omit.hides("speed", &speed));
    3343            4 :     }
    3344              : 
    3345              :     /// Refuses upserts, allows the rest; `short` answers one decision
    3346              :     /// fewer than it was asked for.
    3347              :     struct Batch {
    3348              :         short: bool,
    3349              :     }
    3350              : 
    3351              :     impl PolicyEngine for Batch {
    3352            0 :         fn name(&self) -> &str {
    3353            0 :             "batch"
    3354            0 :         }
    3355           24 :         fn decide<'a>(&'a self, _s: &'a Subject, o: &'a Operation<'a>) -> DecisionFuture<'a> {
    3356           24 :             Box::pin(std::future::ready(if o.clause == "5.6.8" {
    3357           12 :                 Decision::Deny("no upserts".into())
    3358              :             } else {
    3359           12 :                 Decision::Allow
    3360              :             }))
    3361           24 :         }
    3362           12 :         fn decide_each<'a>(
    3363           12 :             &'a self,
    3364           12 :             s: &'a Subject,
    3365           12 :             ops: &'a [Operation<'a>],
    3366           12 :         ) -> DecisionsFuture<'a> {
    3367           12 :             Box::pin(async move {
    3368           12 :                 let mut out = Vec::new();
    3369           24 :                 for op in ops {
    3370           24 :                     out.push(self.decide(s, op).await);
    3371              :                 }
    3372           12 :                 if self.short {
    3373            4 :                     out.pop();
    3374            8 :                 }
    3375           12 :                 out
    3376           12 :             })
    3377           12 :         }
    3378            0 :         fn pre_notify(&self, _s: &Subject, _sub: &Value, _n: &mut Value) -> NotifyDecision {
    3379            0 :             NotifyDecision::Deliver
    3380            0 :         }
    3381              :     }
    3382              : 
    3383              :     #[tokio::test]
    3384            4 :     async fn a_batch_is_answered_member_by_member_and_a_short_answer_denies_all() {
    3385            4 :         let ops = [Operation::new("5.6.7"), Operation::new("5.6.8")];
    3386            4 :         let each = decide_each(&Batch { short: false }, &subject(), &ops).await;
    3387            4 :         assert_eq!(
    3388              :             each,
    3389            4 :             vec![Decision::Allow, Decision::Deny("no upserts".into())]
    3390              :         );
    3391            4 :         let short = decide_each(&Batch { short: true }, &subject(), &ops).await;
    3392            4 :         assert_eq!(short, vec![Decision::Deny(ENGINE_FAILED.to_owned()); 2]);
    3393            4 :         let gated = gate_each(
    3394            4 :             &Batch { short: false },
    3395            4 :             &antares_model::TenantId::default(),
    3396            4 :             &HeaderMap::new(),
    3397            4 :             &ops,
    3398            4 :         )
    3399            4 :         .await;
    3400            4 :         assert_eq!(
    3401            4 :             gated,
    3402            4 :             vec![Ok(Filter::default()), Err("no upserts".to_owned())]
    3403            4 :         );
    3404            4 :     }
    3405              : 
    3406              :     #[tokio::test]
    3407            4 :     async fn the_pre_image_joins_stored_and_written_types_once_each() {
    3408            4 :         let tenant = antares_model::TenantId::default();
    3409            4 :         let id = "urn:ngsi-ld:Car:pre";
    3410            4 :         let stored = json!({"id": id, "type": [iri("Car")],
    3411            4 :                             (iri("speed")): [{"type": "Property", "value": 1}]});
    3412            4 :         let body = json!({"type": [iri("Car"), iri("Vehicle")]});
    3413            4 :         let bare = crate::state::AppState::new("pre-image".into());
    3414            4 :         bare.store
    3415            4 :             .create(&tenant, antares_store::Kind::Entity, id, stored)
    3416            4 :             .await
    3417            4 :             .expect("create");
    3418            4 :         let none = pre_image(&bare, &tenant, id, Some(&body))
    3419            4 :             .await
    3420            4 :             .expect("read");
    3421            4 :         assert!(
    3422            4 :             none.types.is_empty() && none.stored.is_none(),
    3423              :             "no engine, no read"
    3424              :         );
    3425            4 :         let st = bare.with_policy(std::sync::Arc::new(AllowAll));
    3426            4 :         let image = pre_image(&st, &tenant, id, Some(&body))
    3427            4 :             .await
    3428            4 :             .expect("read");
    3429            4 :         assert_eq!(image.types, vec![iri("Car"), iri("Vehicle")]);
    3430            4 :         assert_eq!(
    3431            4 :             image.stored.map(|s| s.attribute_names),
    3432            4 :             Some(vec![iri("speed")])
    3433            4 :         );
    3434            4 :     }
    3435              : 
    3436              :     /// a subject built inside a view's scope names the view, one
    3437              :     /// built outside names none, and no request can carry the pair.
    3438              :     #[tokio::test]
    3439            4 :     async fn a_subject_names_the_view_it_came_through_and_nothing_else_can() {
    3440            4 :         let tenant = antares_model::TenantId::default();
    3441            4 :         let plain = subject_of(&tenant, &HeaderMap::new());
    3442            4 :         assert_eq!(plain.view(), None);
    3443            4 :         let inside = through_view("cars", async { subject_of(&tenant, &HeaderMap::new()) }).await;
    3444            4 :         assert_eq!(inside.view(), Some("cars"));
    3445            4 :         let nested = through_view(
    3446            4 :             "cars",
    3447            4 :             through_view("bikes", async { subject_of(&tenant, &HeaderMap::new()) }),
    3448              :         )
    3449            4 :         .await;
    3450            4 :         assert_eq!(nested.view(), Some("bikes"), "the innermost view decides");
    3451            4 :         assert!(
    3452            4 :             axum::http::HeaderName::from_bytes(VIEW_PAIR.as_bytes()).is_err(),
    3453            4 :             "{VIEW_PAIR} must not be a header name a client could send"
    3454            4 :         );
    3455            4 :     }
    3456              : 
    3457              :     /// the view is stored with the subject (a notification is
    3458              :     /// decided for it), and a condition never reads it as one of the
    3459              :     /// creator's values.
    3460              :     #[test]
    3461            4 :     fn the_view_is_stored_with_the_subject_and_never_read_as_its_value() {
    3462            4 :         let tenant = antares_model::TenantId::default();
    3463            4 :         let subject = Subject {
    3464            4 :             tenant: tenant.clone(),
    3465            4 :             headers: vec![
    3466            4 :                 ("x-caller-subject".into(), "ana".into()),
    3467            4 :                 (VIEW_PAIR.into(), "ana".into()),
    3468            4 :             ],
    3469            4 :         };
    3470            4 :         let mut doc = json!({"id": "urn:ngsi-ld:Subscription:1", "type": "Subscription"});
    3471            4 :         if let (Some(o), Some(member)) = (doc.as_object_mut(), subject_member(&subject)) {
    3472            4 :             o.insert(SUBJECT_MEMBER.into(), member);
    3473            4 :         }
    3474            4 :         let stored = stored_subject(&tenant, &doc);
    3475            4 :         assert_eq!(stored.view(), Some("ana"));
    3476            4 :         assert!(belongs_to(&doc, &subject));
    3477            4 :         let through_other = Subject {
    3478            4 :             tenant: tenant.clone(),
    3479            4 :             headers: vec![
    3480            4 :                 ("x-caller-subject".into(), "ana".into()),
    3481            4 :                 (VIEW_PAIR.into(), "bikes".into()),
    3482            4 :             ],
    3483            4 :         };
    3484            4 :         assert!(
    3485            4 :             !belongs_to(&doc, &through_other),
    3486              :             "another view, another creator"
    3487              :         );
    3488            4 :         let image = record_image(&doc);
    3489            4 :         let values: Vec<&Value> = image
    3490            4 :             .as_object()
    3491            4 :             .into_iter()
    3492            4 :             .flatten()
    3493           12 :             .filter(|(k, _)| k.ends_with("subject"))
    3494            4 :             .flat_map(|(_, v)| v.as_array().into_iter().flatten())
    3495            4 :             .collect();
    3496            4 :         assert_eq!(values, [&json!({"value": "ana"})], "{image}");
    3497            4 :     }
    3498              : 
    3499              :     /// the store decides a view's reads only when every typed rule
    3500              :     /// narrows by the same translatable `idPattern` set and nothing else;
    3501              :     /// each guard of `shared_id_patterns` has its case.
    3502              :     #[test]
    3503            4 :     fn only_one_shared_translatable_pattern_set_is_left_to_the_store() {
    3504              :         const TREES: &str =
    3505              :             "^urn:ngsi-ld:Tree:[a-z0-9.-]+:town-centre:green-[a-z-]+-[0-9a-f-]{36}$";
    3506            4 :         let tree = |patterns: &[&str]| Rule {
    3507           56 :             id_patterns: patterns.iter().map(|p| (*p).to_owned()).collect(),
    3508           56 :             ..rule("Tree", &["name"])
    3509           56 :         };
    3510            4 :         let untyped = Rule {
    3511            4 :             types: Vec::new(),
    3512            4 :             q: Some(Box::new(antares_ql::parse_q("height>5").expect("q"))),
    3513            4 :             ..Rule::default()
    3514            4 :         };
    3515            4 :         let seeded = [tree(&[TREES]), untyped.clone()];
    3516            4 :         assert_eq!(
    3517            4 :             shared_id_patterns(&seeded),
    3518            4 :             Some([TREES.to_owned()].as_slice())
    3519              :         );
    3520            4 :         let refused: [(&str, Vec<Rule>); 10] = [
    3521            4 :             (
    3522            4 :                 "q",
    3523            4 :                 vec![Rule {
    3524            4 :                     q: Some(Box::new(antares_ql::parse_q("height>5").expect("q"))),
    3525            4 :                     ..tree(&[TREES])
    3526            4 :                 }],
    3527            4 :             ),
    3528            4 :             (
    3529            4 :                 "scopeQ",
    3530            4 :                 vec![Rule {
    3531            4 :                     scope_q: Some("/town".into()),
    3532            4 :                     ..tree(&[TREES])
    3533            4 :                 }],
    3534            4 :             ),
    3535            4 :             (
    3536            4 :                 "geo_q",
    3537            4 :                 vec![Rule {
    3538            4 :                     geo_q: Some(zone_a()),
    3539            4 :                     ..tree(&[TREES])
    3540            4 :                 }],
    3541            4 :             ),
    3542            4 :             (
    3543            4 :                 "geo_also",
    3544            4 :                 vec![Rule {
    3545            4 :                     geo_also: vec![*zone_a()],
    3546            4 :                     ..tree(&[TREES])
    3547            4 :                 }],
    3548            4 :             ),
    3549            4 :             (
    3550            4 :                 "ids",
    3551            4 :                 vec![Rule {
    3552            4 :                     ids: vec!["urn:ngsi-ld:Tree:1".into()],
    3553            4 :                     ..tree(&[TREES])
    3554            4 :                 }],
    3555            4 :             ),
    3556            4 :             (
    3557            4 :                 "other patterns",
    3558            4 :                 vec![tree(&[TREES]), tree(&["^urn:ngsi-ld:Tree:other-[0-9]+$"])],
    3559            4 :             ),
    3560            4 :             ("no pattern after one", vec![tree(&[TREES]), tree(&[])]),
    3561            4 :             ("no pattern before one", vec![tree(&[]), tree(&[TREES])]),
    3562            4 :             ("no pattern", vec![tree(&[]), untyped]),
    3563            4 :             ("untranslatable", vec![tree(&["urn:x:.*"])]),
    3564            4 :         ];
    3565           40 :         for (case, rules) in &refused {
    3566           40 :             assert_eq!(shared_id_patterns(rules), None, "{case}");
    3567              :         }
    3568            4 :     }
    3569              : }
        

Generated by: LCOV version 2.0-1