Line data Source code
1 : // SPDX-License-Identifier: EUPL-1.2
2 : //! /csourceRegistrations (5.9, 5.10; resources 6.8/6.9).
3 :
4 : use crate::negotiate::*;
5 : use crate::registry::{
6 : csf_matches, csr_matches, ei_types, present_registration, reg_expired,
7 : temporal_interval_matches, CsrSpec,
8 : };
9 : use crate::state::{now_iso, AppState};
10 : use antares_jsonld::{parse_datetime, Context};
11 : use antares_model::operations::{OPERATION_GROUPS, OPERATION_NAMES};
12 : use antares_model::{NgsiError, TenantId};
13 : use antares_store::CurrentStateDriverExt;
14 : use antares_store::Kind;
15 : use axum::body::Bytes;
16 : use axum::extract::{Path, State};
17 : use axum::http::{HeaderMap, StatusCode};
18 : use axum::response::{IntoResponse, Response};
19 : use serde_json::{Map, Value};
20 :
21 : use crate::negotiate::CleanParams;
22 :
23 : /// Cardinality caps on a CSourceRegistration. Generous against any real
24 : /// federation topology (a tenant is sized at 1000+ registrations, not one
25 : /// registration at 1000+ selectors) and small enough that the worst case is
26 : /// MAX_INFORMATION × MAX_INFO_MEMBERS² index rows, not 10^10.
27 : const MAX_INFORMATION: usize = 128;
28 : const MAX_INFO_MEMBERS: usize = 128;
29 : /// Table 5.2.10-1 RegistrationInfo: the `information` member of a
30 : /// Context Source Registration (5.2.9). Each entry names what the
31 : /// source holds — entities, property names, relationship names — and
32 : /// every name is expanded, because matching (5.11.2) compares IRIs.
33 3192 : fn norm_registration_info(v: &Value, ctx: &Context) -> Result<Value, NgsiError> {
34 3192 : let bad = NgsiError::BadRequestData;
35 3192 : let arr = v
36 3192 : .as_array()
37 3192 : .filter(|a| !a.is_empty())
38 3192 : .ok_or_else(|| bad("information must be a non-empty array (5.2.9)".into()))?;
39 : // The csource_index explosion is |entities| ×
40 : // (|propertyNames| + |relationshipNames|) PER information
41 : // element, materialised in memory before any SQL runs. Under
42 : // only the 4 MiB body cap that is ~10^10 objects — an OOM from
43 : // one request. Cardinality is capped at the validation
44 : // boundary, where the error is a 400 and not a dead pod:
45 : // there is no query here to be too complex, and 5.9.2.4 gives
46 : // BadRequestData for a registration whose content is refused.
47 3192 : if arr.len() > MAX_INFORMATION {
48 4 : return Err(bad(format!(
49 4 : "information has {} entries (limit {MAX_INFORMATION})",
50 4 : arr.len()
51 4 : )));
52 3188 : }
53 3188 : let mut infos = Vec::new();
54 3696 : for info in arr {
55 3696 : let io = info
56 3696 : .as_object()
57 3696 : .ok_or_else(|| bad("information entries must be objects".into()))?;
58 11068 : for key in ["entities", "propertyNames", "relationshipNames"] {
59 11068 : if let Some(n) = io.get(key).and_then(Value::as_array).map(Vec::len) {
60 5508 : if n > MAX_INFO_MEMBERS {
61 16 : return Err(bad(format!(
62 16 : "information.{key} has {n} entries (limit {MAX_INFO_MEMBERS})"
63 16 : )));
64 5492 : }
65 5560 : }
66 : }
67 3680 : let mut ni = Map::new();
68 5484 : for (ik, iv) in io {
69 5484 : match ik.as_str() {
70 5484 : "entities" => {
71 3656 : let es = iv
72 3656 : .as_array()
73 3656 : .filter(|a| !a.is_empty())
74 3656 : .ok_or_else(|| bad("entities must be a non-empty array".into()))?;
75 3652 : let mut nes = Vec::new();
76 4164 : for e in es {
77 4164 : let eo = e
78 4164 : .as_object()
79 4164 : .ok_or_else(|| bad("entities entries must be objects".into()))?;
80 4164 : let mut ne = Map::new();
81 6998 : for (ek, ev) in eo {
82 6998 : match ek.as_str() {
83 : // 5.2.8: type is "String or String[]" — both forms legal.
84 6998 : "type" => {
85 4140 : let expand_one = |t: &Value| -> Result<Value, NgsiError> {
86 4140 : let t = t.as_str().filter(|t| !t.is_empty()).ok_or_else(|| {
87 0 : bad("EntityInfo type must be a non-empty string (5.2.8)".into())
88 0 : })?;
89 4140 : Ok(Value::String(ctx.expand_key(t)))
90 4140 : };
91 4132 : let expanded = match ev {
92 20 : Value::Array(ts) if !ts.is_empty() => Value::Array(
93 16 : ts.iter().map(expand_one).collect::<Result<_, _>>()?,
94 : ),
95 : Value::Array(_) => {
96 4 : return Err(bad(
97 4 : "EntityInfo type array must not be empty (5.2.8)"
98 4 : .into(),
99 4 : ))
100 : }
101 4116 : other => expand_one(other)?,
102 : };
103 4132 : ne.insert("type".into(), expanded);
104 : }
105 2862 : "id" => {
106 1948 : let id = ev
107 1948 : .as_str()
108 1948 : .ok_or_else(|| bad("EntityInfo id must be a URI".into()))?;
109 1948 : antares_model::EntityId::new(id)?;
110 1944 : ne.insert("id".into(), ev.clone());
111 : }
112 914 : "idPattern" => {
113 914 : let p = ev
114 914 : .as_str()
115 914 : .ok_or_else(|| bad("idPattern must be a string".into()))?;
116 914 : antares_ql::regex::compile(p)
117 914 : .map_err(|_| bad(format!("invalid idPattern {p:?}")))?;
118 910 : ne.insert("idPattern".into(), ev.clone());
119 : }
120 0 : _ => {
121 0 : ne.insert(ek.clone(), ev.clone());
122 0 : }
123 : }
124 : }
125 : // type is optional in EntityInfo when an
126 : // id/idPattern identifies the entities
127 4152 : if !ne.contains_key("type")
128 20 : && !ne.contains_key("id")
129 20 : && !ne.contains_key("idPattern")
130 : {
131 0 : return Err(bad(
132 0 : "EntityInfo requires type, id or idPattern (5.2.8)".into()
133 0 : ));
134 4152 : }
135 4152 : nes.push(Value::Object(ne));
136 : }
137 3640 : ni.insert("entities".into(), Value::Array(nes));
138 : }
139 1828 : "propertyNames" | "relationshipNames" => {
140 : // 5.2.10: "Empty array is not allowed"
141 1828 : let names = iv
142 1828 : .as_array()
143 1828 : .filter(|a| !a.is_empty())
144 1828 : .ok_or_else(|| bad(format!("{ik} must be a non-empty array (5.2.10)")))?;
145 1820 : let mut nn = Vec::new();
146 2840 : for n in names {
147 2840 : let s = n
148 2840 : .as_str()
149 2840 : .ok_or_else(|| bad(format!("{ik} entries must be strings")))?;
150 2840 : nn.push(Value::String(ctx.expand_key(s)));
151 : }
152 1820 : ni.insert(ik.clone(), Value::Array(nn));
153 : }
154 0 : _ => {
155 0 : ni.insert(ik.clone(), iv.clone());
156 0 : }
157 : }
158 : }
159 3656 : infos.push(Value::Object(ni));
160 : }
161 3148 : Ok(Value::Array(infos))
162 3192 : }
163 :
164 : /// 4.3.6.5 and Table 5.2.22-1: the `contextSourceInfo` pairs a broker
165 : /// sends when it contacts the source. Every pair becomes an HTTP header
166 : /// on a forward, and the four keys 4.3.6.6 processes have value spaces
167 : /// of their own.
168 184 : fn check_context_source_info(v: &Value) -> Result<(), NgsiError> {
169 184 : let bad = NgsiError::BadRequestData;
170 184 : let arr = v
171 184 : .as_array()
172 184 : .ok_or_else(|| bad("contextSourceInfo must be an array (5.2.9)".into()))?;
173 186 : for kv in arr {
174 186 : let Some(key) = kv.get("key").and_then(Value::as_str) else {
175 8 : return Err(bad(
176 8 : "contextSourceInfo entries must be {key, value} pairs (5.2.22)".into(),
177 8 : ));
178 : };
179 : // Table 5.2.22-1: value is a String, cardinality 1.
180 178 : let Some(value) = kv.get("value").filter(|v| v.is_string()) else {
181 28 : return Err(bad(
182 28 : "contextSourceInfo entries must be {key, value} pairs of Strings (5.2.22)".into(),
183 28 : ));
184 : };
185 : // 6.3.19: "Key and value members shall adhere to IETF
186 : // RFC 7230 definitions concerning HTTP headers". The pair
187 : // becomes a header on every forward, so the transport's
188 : // own RFC 7230 parsers are the judge — a name or a value
189 : // they refuse can only fail later, at a forward whose
190 : // error names no registration.
191 150 : if !crate::negotiate::is_field_name(key) {
192 40 : return Err(bad(format!(
193 40 : "contextSourceInfo key {key:?} is not an RFC 7230 header name (6.3.19)"
194 40 : )));
195 110 : }
196 110 : if !value.as_str().is_some_and(crate::negotiate::is_field_value) {
197 24 : return Err(bad(format!(
198 24 : "contextSourceInfo value for {key:?} is not an RFC 7230 header value \
199 24 : (6.3.19)"
200 24 : )));
201 86 : }
202 : // 4.3.6.6: the four processed keys have constrained
203 : // value spaces — reject bad ones at registration, not at
204 : // first forward
205 86 : let sval = value.as_str();
206 86 : match key.to_ascii_lowercase().as_str() {
207 86 : "accept" | "contenttype" => {
208 16 : if !matches!(sval, Some("application/json" | "application/ld+json")) {
209 8 : return Err(bad(format!(
210 8 : "contextSourceInfo {key} must be application/json or \
211 8 : application/ld+json (4.3.6.6)"
212 8 : )));
213 8 : }
214 : }
215 70 : "jsonldcontext" => {
216 20 : if sval.is_none_or(|s| antares_model::EntityId::new(s).is_err()) {
217 4 : return Err(bad(
218 4 : "contextSourceInfo jsonldContext must be a URL (4.3.6.6)".into(),
219 4 : ));
220 16 : }
221 : }
222 50 : "ngsildconformance"
223 8 : if sval.is_none_or(|s| crate::conformance::parse_version(s).is_none()) =>
224 : {
225 4 : return Err(bad(
226 4 : "contextSourceInfo ngsildConformance must be major.minor \
227 4 : (4.3.6.6)"
228 4 : .into(),
229 4 : ));
230 : }
231 46 : _ => {}
232 : }
233 : }
234 68 : Ok(())
235 184 : }
236 :
237 : /// Table 5.2.9-1: `operations` entries "are limited to the named API
238 : /// operations and named operation groups (see clause 4.20)".
239 672 : fn check_operations(v: &Value) -> Result<(), NgsiError> {
240 672 : let bad = NgsiError::BadRequestData;
241 672 : let arr = v
242 672 : .as_array()
243 672 : .filter(|a| !a.is_empty())
244 672 : .ok_or_else(|| bad("operations must be a non-empty array (5.2.9)".into()))?;
245 952 : for op in arr {
246 952 : let name = op.as_str().unwrap_or_default();
247 952 : if !OPERATION_NAMES.contains(&name) && !OPERATION_GROUPS.contains(&name) {
248 28 : return Err(bad(format!(
249 28 : "unknown operation {name:?} — entries are limited to the \
250 28 : 4.20 names and groups (5.2.9)"
251 28 : )));
252 924 : }
253 : }
254 636 : Ok(())
255 672 : }
256 :
257 : /// Table 5.2.34-1 RegistrationManagementInfo: `cacheDuration` an ISO 8601
258 : /// duration, `cooldown` and `timeout` numbers greater than 0, `localOnly`
259 : /// a Boolean.
260 56 : fn check_registration_management(v: &Value) -> Result<(), NgsiError> {
261 56 : let bad = NgsiError::BadRequestData;
262 56 : let m = v.as_object().ok_or_else(|| {
263 8 : bad("management must be a RegistrationManagementInfo object (5.2.34)".into())
264 8 : })?;
265 48 : if let Some(d) = m.get("cacheDuration") {
266 20 : if !d.as_str().is_some_and(valid_iso8601_duration) {
267 12 : return Err(bad(
268 12 : "management cacheDuration must be an ISO 8601 duration (5.2.34)".into(),
269 12 : ));
270 8 : }
271 28 : }
272 64 : for key in ["cooldown", "timeout"] {
273 64 : if let Some(n) = m.get(key) {
274 36 : if !n.as_f64().is_some_and(|n| n > 0.0) {
275 20 : return Err(bad(format!(
276 20 : "management {key} must be a number greater than 0 (5.2.34)"
277 20 : )));
278 16 : }
279 28 : }
280 : }
281 16 : if let Some(l) = m.get("localOnly") {
282 16 : if !l.is_boolean() {
283 8 : return Err(bad("management localOnly must be a boolean (5.2.34)".into()));
284 8 : }
285 0 : }
286 8 : Ok(())
287 56 : }
288 :
289 : /// One member of Table 5.2.9-1, validated and normalized into `out`.
290 : /// A member the table does not name is kept verbatim: 5.5.9 asks a
291 : /// receiver to tolerate what it does not know.
292 16336 : fn norm_reg_member(
293 16336 : k: &str,
294 16336 : v: &Value,
295 16336 : ctx: &Context,
296 16336 : out: &mut Map<String, Value>,
297 16336 : ) -> Result<(), NgsiError> {
298 16336 : let bad = NgsiError::BadRequestData;
299 16336 : match k {
300 : // Table 5.2.9-2's members are read-only and "shall be automatically
301 : // generated by NGSI-LD implementations. In the event that they are
302 : // provided (in update or create operations) NGSI-LD implementations
303 : // shall ignore them" — dropped here rather than refused, which is
304 : // what "ignore" says, alongside the 4.8 system attributes.
305 16336 : "@context" | "createdAt" | "modifiedAt" | "status" | "timesSent" | "timesFailed"
306 15424 : | "lastSuccess" | "lastFailure" => return Ok(()),
307 15392 : "id" => {
308 3138 : let id = v
309 3138 : .as_str()
310 3138 : .ok_or_else(|| bad("registration id must be a string URI".into()))?;
311 3138 : antares_model::EntityId::new(id)?;
312 3138 : out.insert("id".into(), v.clone());
313 : }
314 12254 : "type" => {
315 3028 : if v.as_str() != Some("ContextSourceRegistration") {
316 0 : return Err(bad(
317 0 : "type must be \"ContextSourceRegistration\" (5.2.9)".into()
318 0 : ));
319 3028 : }
320 3028 : out.insert("type".into(), v.clone());
321 : }
322 9226 : "information" => {
323 3192 : out.insert("information".into(), norm_registration_info(v, ctx)?);
324 : }
325 6034 : "mode" => {
326 1676 : let m = v
327 1676 : .as_str()
328 1676 : .filter(|m| ["inclusive", "auxiliary", "exclusive", "redirect"].contains(m))
329 1676 : .ok_or_else(|| {
330 4 : bad("mode must be inclusive, auxiliary, exclusive or redirect (5.2.9)".into())
331 4 : })?;
332 1672 : out.insert("mode".into(), Value::String(m.to_owned()));
333 : }
334 4358 : "endpoint" => {
335 3238 : let uri = v
336 3238 : .as_str()
337 3238 : .ok_or_else(|| bad("endpoint must be a URI string".into()))?;
338 3238 : antares_model::EntityId::new(uri)
339 3238 : .map_err(|_| bad(format!("endpoint is not a valid URI: {uri:?}")))?;
340 3238 : out.insert("endpoint".into(), v.clone());
341 : }
342 1120 : "expiresAt" => {
343 58 : let s = v
344 58 : .as_str()
345 58 : .filter(|s| parse_datetime(s))
346 58 : .ok_or_else(|| bad("expiresAt must be an ISO 8601 DateTime".into()))?;
347 : // the instant decides, not the spelling: now_iso always
348 : // carries 3 fraction digits, a client's expiresAt 0 to 6
349 54 : if antares_model::dt_key(s) < antares_model::dt_key(&now_iso()) {
350 36 : return Err(bad("expiresAt is in the past".into()));
351 18 : }
352 18 : out.insert("expiresAt".into(), v.clone());
353 : }
354 : // 5.2.9 `tenant`: the Tenant to use in all requests to this
355 : // source — validated with the same rules as the header (4.14).
356 1062 : "tenant" => {
357 8 : let t = v
358 8 : .as_str()
359 8 : .ok_or_else(|| bad("tenant must be a string (5.2.9)".into()))?;
360 8 : antares_model::TenantId::new(t)?;
361 8 : out.insert("tenant".into(), v.clone());
362 : }
363 : // 4.3.6.5: KeyValuePair[] conveyed when contacting the source.
364 1054 : "contextSourceInfo" => {
365 184 : check_context_source_info(v)?;
366 68 : out.insert("contextSourceInfo".into(), v.clone());
367 : }
368 : // Table 5.2.9-1: operations entries "are limited to the named
369 : // API operations and named operation groups (see clause 4.20)".
370 870 : "operations" => {
371 672 : check_operations(v)?;
372 636 : out.insert("operations".into(), v.clone());
373 : }
374 : // 4.3.6.4 / 5.2.9: localOnly is a Boolean.
375 198 : "localOnly" => {
376 10 : if !v.is_boolean() {
377 4 : return Err(bad("localOnly must be a boolean (5.2.9)".into()));
378 6 : }
379 6 : out.insert("localOnly".into(), v.clone());
380 : }
381 : // Table 5.2.9-1: a non-empty RFC 7230 pseudonym token.
382 188 : "contextSourceAlias" => {
383 16 : let a = v
384 16 : .as_str()
385 16 : .filter(|a| crate::negotiate::is_field_name(a))
386 16 : .ok_or_else(|| {
387 8 : bad(
388 8 : "contextSourceAlias must be a non-empty RFC 7230 pseudonym token \
389 8 : (5.2.9)"
390 8 : .into(),
391 8 : )
392 8 : })?;
393 8 : out.insert("contextSourceAlias".into(), Value::String(a.to_owned()));
394 : }
395 : // Table 5.2.9-1: non-empty strings.
396 172 : "description" | "registrationName" => {
397 8 : if v.as_str().is_none_or(str::is_empty) {
398 8 : return Err(bad(format!("{k} must be a non-empty string (5.2.9)")));
399 0 : }
400 0 : out.insert(k.to_owned(), v.clone());
401 : }
402 : // Table 5.2.9-1: valid URIs, "@none" for the default instances.
403 164 : "datasetId" => {
404 10 : let arr = v
405 10 : .as_array()
406 10 : .ok_or_else(|| bad("datasetId must be an array of URIs (5.2.9)".into()))?;
407 14 : for d in arr {
408 14 : let d = d.as_str().unwrap_or_default();
409 14 : if d != "@none" && antares_model::EntityId::new(d).is_err() {
410 4 : return Err(bad(format!("datasetId entry {d:?} is not a URI (5.2.9)")));
411 10 : }
412 : }
413 6 : out.insert("datasetId".into(), v.clone());
414 : }
415 : // Table 5.2.9-1: scope(s) per the 4.18 grammar.
416 154 : "scope" => {
417 20 : let all_valid = match v {
418 12 : Value::String(s) => antares_jsonld::valid_scope_value(s),
419 8 : Value::Array(a) => a
420 8 : .iter()
421 16 : .all(|s| s.as_str().is_some_and(antares_jsonld::valid_scope_value)),
422 0 : _ => false,
423 : };
424 20 : if !all_valid {
425 4 : return Err(bad("scope violates the 4.18 grammar (5.2.9)".into()));
426 16 : }
427 16 : out.insert("scope".into(), v.clone());
428 : }
429 : // Table 5.2.9-1: GeoJSON geometries per 4.7.
430 : // Table 5.2.9-1: each is a GeoJSON geometry (4.7).
431 134 : "location" | "observationSpace" | "operationSpace" => {
432 12 : let ok = v
433 12 : .as_object()
434 12 : .and_then(|o| Some((o.get("type")?.as_str()?, o.get("coordinates")?)))
435 12 : .is_some_and(|(t, c)| antares_ql::geo::parse_ref_geometry(t, c).is_ok());
436 12 : if !ok {
437 4 : return Err(bad(format!("{k} must be a 4.7 GeoJSON geometry (5.2.9)")));
438 8 : }
439 8 : out.insert(k.to_owned(), v.clone());
440 : }
441 : // Table 5.2.34-1 (RegistrationManagementInfo): cacheDuration an
442 : // ISO 8601 duration, cooldown/timeout numbers greater than 0,
443 : // localOnly a boolean.
444 122 : "management" => {
445 56 : check_registration_management(v)?;
446 8 : out.insert("management".into(), v.clone());
447 : }
448 : // Table 5.2.9-1: an ISO 8601 duration.
449 66 : "refreshRate" => {
450 8 : let ok = v.as_str().is_some_and(valid_iso8601_duration);
451 8 : if !ok {
452 4 : return Err(bad(
453 4 : "refreshRate must be an ISO 8601 duration (5.2.9)".into()
454 4 : ));
455 4 : }
456 4 : out.insert("refreshRate".into(), v.clone());
457 : }
458 58 : "observationInterval" | "managementInterval" => {
459 20 : let o = v
460 20 : .as_object()
461 20 : .ok_or_else(|| bad(format!("{k} must be a TimeInterval object")))?;
462 20 : let start = o
463 20 : .get("startAt")
464 20 : .and_then(Value::as_str)
465 20 : .filter(|s| parse_datetime(s));
466 20 : if start.is_none() {
467 8 : return Err(bad(format!("{k}.startAt must be an ISO 8601 DateTime")));
468 12 : }
469 12 : if let Some(e) = o.get("endAt") {
470 8 : e.as_str()
471 8 : .filter(|s| parse_datetime(s))
472 8 : .ok_or_else(|| bad(format!("{k}.endAt must be an ISO 8601 DateTime")))?;
473 4 : }
474 8 : out.insert(k.to_owned(), v.clone());
475 : }
476 38 : _ => {
477 38 : // tolerant reader: keep unknown members
478 38 : out.insert(k.to_owned(), v.clone());
479 38 : }
480 : }
481 15056 : Ok(())
482 16336 : }
483 :
484 : /// The Table 5.2.9-1 rules that hold between members rather than over
485 : /// one: what a Context Source Registration must carry, and the modes
486 : /// that exclude each other.
487 3464 : fn check_registration_members(out: &Map<String, Value>, is_patch: bool) -> Result<(), NgsiError> {
488 3464 : let bad = NgsiError::BadRequestData;
489 3464 : if !is_patch {
490 3028 : if !out.contains_key("type") {
491 0 : return Err(bad(
492 0 : "type must be \"ContextSourceRegistration\" (5.2.9)".into()
493 0 : ));
494 3028 : }
495 3028 : if !out.contains_key("endpoint") {
496 0 : return Err(bad("endpoint is required (5.2.9)".into()));
497 3028 : }
498 3028 : if !out.contains_key("information") {
499 0 : return Err(bad("information is required (5.2.9)".into()));
500 3028 : }
501 3028 : validate_exclusive(out)?;
502 436 : }
503 3452 : Ok(())
504 3464 : }
505 :
506 : /// Validate + normalize a CSourceRegistration (5.2.9): types and attribute
507 : /// names inside `information` expand to IRIs.
508 3804 : pub fn normalize_registration(
509 3804 : doc: &Map<String, Value>,
510 3804 : ctx: &Context,
511 3804 : is_patch: bool,
512 3804 : ) -> Result<Map<String, Value>, NgsiError> {
513 3804 : let bad = NgsiError::BadRequestData;
514 : // 5.5.4: first-level member nulls are only legal in fragments (patch)
515 3804 : if !is_patch {
516 3368 : antares_jsonld::reject_first_level_nulls(doc)?;
517 436 : }
518 3800 : let mut out = Map::new();
519 16340 : for (k, v) in doc {
520 : // NGSI-LD Fragment member removal (5.4): null / NGSI-LD Null
521 16340 : if is_patch && k != "id" && (v.is_null() || v.as_str() == Some("urn:ngsi-ld:null")) {
522 4 : if ["type", "information", "endpoint"].contains(&k.as_str()) {
523 0 : return Err(bad(format!("cannot remove mandatory member {k} (5.9.3)")));
524 4 : }
525 4 : out.insert(k.clone(), Value::Null);
526 4 : continue;
527 16336 : }
528 16336 : norm_reg_member(k, v, ctx, &mut out)?;
529 : }
530 3464 : check_registration_members(&out, is_patch)?;
531 3452 : Ok(out)
532 3804 : }
533 :
534 : /// ISO 8601 duration (5.2.9 refreshRate): `P[nY][nM][nW][nD][T[nH][nM][nS]]`,
535 : /// at least one component, digits (fraction allowed in seconds).
536 148 : fn valid_iso8601_duration(s: &str) -> bool {
537 148 : antares_model::parse_iso_duration(s).is_some_and(|d| !d.empty)
538 148 : }
539 :
540 : /// 5.9.2.4: an auxiliary registration may only offer "retrieveOps",
541 : /// "retrieveEntity" or "queryEntity" (or a combination thereof) — enforced
542 : /// when the operations member is present (absent = deployment default).
543 3100 : fn validate_auxiliary_ops(doc: &Map<String, Value>) -> Result<(), NgsiError> {
544 3100 : if doc.get("mode").and_then(Value::as_str) != Some("auxiliary") {
545 3084 : return Ok(());
546 16 : }
547 16 : let Some(ops) = doc.get("operations").and_then(Value::as_array) else {
548 0 : return Ok(());
549 : };
550 16 : let allowed = ["retrieveOps", "retrieveEntity", "queryEntity"];
551 16 : if let Some(bad_op) = ops
552 16 : .iter()
553 16 : .filter_map(Value::as_str)
554 20 : .find(|o| !allowed.contains(o))
555 : {
556 8 : return Err(NgsiError::BadRequestData(format!(
557 8 : "auxiliary registration operations are limited to \
558 8 : retrieveOps/retrieveEntity/queryEntity — {bad_op:?} is not allowed (5.9.2.4)"
559 8 : )));
560 8 : }
561 8 : Ok(())
562 3100 : }
563 :
564 : /// 5.9.2.4 registration-vs-entity conflicts. Exclusive: "If an Entity
565 : /// already exists for the supplied Entity ID (URI) and the existing Entity
566 : /// contains any of the Attributes defined in the registration, an error of
567 : /// type Conflict shall be raised." Redirect: "If an existing Entity already
568 : /// matches the Context Source Registration, an error of type Conflict shall
569 : /// be raised."
570 : ///
571 : /// Read shape, not read volume: this runs under the process-wide
572 : /// registration write lock, so a fold of the tenant here stalls every other
573 : /// registration write on the broker, and a whole-tenant query above the
574 : /// store's row ceiling (5.5.6) would refuse a registration create that has
575 : /// no TooManyResults to raise. An EntityInfo that names a concrete id is
576 : /// answered by reading that Entity — the only shape an exclusive
577 : /// registration has, since 4.3.6.3 requires an entity id. Everything else
578 : /// (an `idPattern`, or a type alone) is answered by walking the tenant a
579 : /// page at a time, asking every EntityInfo about each Entity as it arrives
580 : /// rather than re-reading the tenant per EntityInfo.
581 3140 : async fn check_entity_conflict(
582 3140 : st: &AppState,
583 3140 : tenant: &antares_model::TenantId,
584 3140 : doc: &Map<String, Value>,
585 3140 : ) -> Result<(), NgsiError> {
586 3140 : let mode = doc
587 3140 : .get("mode")
588 3140 : .and_then(Value::as_str)
589 3140 : .unwrap_or("inclusive");
590 3140 : if mode != "exclusive" && mode != "redirect" {
591 1752 : return Ok(());
592 1388 : }
593 1388 : let infos = doc
594 1388 : .get("information")
595 1388 : .and_then(Value::as_array)
596 1388 : .map(Vec::as_slice)
597 1388 : .unwrap_or_default();
598 1388 : for info in infos {
599 1388 : let attrs: Vec<String> = ["propertyNames", "relationshipNames"]
600 1388 : .iter()
601 2776 : .flat_map(|k| info.get(*k).and_then(Value::as_array).into_iter().flatten())
602 1388 : .filter_map(Value::as_str)
603 1388 : .map(str::to_owned)
604 1388 : .collect();
605 1388 : let ents = info
606 1388 : .get("entities")
607 1388 : .and_then(Value::as_array)
608 1388 : .map(Vec::as_slice)
609 1388 : .unwrap_or_default();
610 1388 : if ents.is_empty() {
611 0 : continue;
612 1388 : }
613 : // Every selector of this RegistrationInfo, its idPattern compiled
614 : // once: an Entity is read at most once and asked about all of them.
615 1388 : let wants: Vec<_> = ents
616 1388 : .iter()
617 1392 : .map(|e| {
618 : (
619 1392 : e.get("id").and_then(Value::as_str),
620 : // 5.2.8: an EntityInfo type is a String or a String[]
621 1392 : ei_types(e),
622 1392 : e.get("idPattern")
623 1392 : .and_then(Value::as_str)
624 1392 : .and_then(|p| antares_ql::regex::compile(p).ok()),
625 : )
626 1392 : })
627 1388 : .collect();
628 4072 : let hit = |existing: &Value| -> Option<NgsiError> {
629 4072 : let eid = existing.get("id").and_then(Value::as_str).unwrap_or("");
630 4076 : for (want_id, want_types, pattern) in &wants {
631 4076 : let id_hit = match (want_id, pattern) {
632 24 : (Some(w), _) => *w == eid,
633 4036 : (None, Some(re)) => re.is_match(eid),
634 16 : (None, None) => true,
635 : };
636 4076 : if !id_hit {
637 4020 : continue;
638 56 : }
639 56 : if !want_types.is_empty() {
640 44 : let matches_type =
641 44 : existing
642 44 : .get("type")
643 44 : .and_then(Value::as_array)
644 44 : .is_some_and(|ts| {
645 44 : ts.iter()
646 44 : .filter_map(Value::as_str)
647 44 : .any(|x| want_types.contains(&x))
648 44 : });
649 44 : if !matches_type {
650 12 : continue;
651 32 : }
652 12 : }
653 44 : let conflict = match mode {
654 : // exclusive names concrete Attributes (4.3.6.3) — only
655 : // an entity already carrying one of them conflicts
656 44 : "exclusive" => attrs.iter().any(|a| existing.get(a).is_some()),
657 : // redirect: any matching entity conflicts
658 28 : _ => true,
659 : };
660 44 : if conflict {
661 36 : return Some(NgsiError::Conflict(format!(
662 36 : "existing entity {eid} conflicts with the {mode} registration (5.9.2.4)"
663 36 : )));
664 8 : }
665 : }
666 4036 : None
667 4072 : };
668 :
669 1388 : let ids: Vec<&str> = ents
670 1388 : .iter()
671 1392 : .filter_map(|e| e.get("id").and_then(Value::as_str))
672 1388 : .collect();
673 1388 : if ids.len() == ents.len() && ents.iter().all(|e| e.get("idPattern").is_none()) {
674 : // Every selector names one Entity, so the read is those Entities
675 : // and nothing else — bounded by the registration, not by the
676 : // tenant, whatever the tenant holds.
677 1340 : for id in &ids {
678 1340 : if let Some(existing) = st.store.get(tenant, Kind::Entity, id).await? {
679 24 : if let Some(conflict) = hit(&existing) {
680 16 : return Err(conflict);
681 8 : }
682 1316 : }
683 : }
684 : } else {
685 : // A pattern (or a type alone) can only be answered by the
686 : // Entities of the tenant. The walk stops at the first conflict.
687 : // ponytail: O(tenant) reads under the registration write lock for
688 : // a pattern selector; narrow the walk to the id range of
689 : // `filter::id_pattern_literal` when every selector of the
690 : // RegistrationInfo carries an anchored literal.
691 4048 : walk_docs(st, tenant, Kind::Entity, |existing| match hit(&existing) {
692 20 : Some(conflict) => Err(conflict),
693 4028 : None => Ok(()),
694 4048 : })
695 48 : .await?;
696 : }
697 : }
698 1352 : Ok(())
699 3140 : }
700 :
701 : /// The stored registration for `id`, with 5.9.2.4's deletion applied: "If
702 : /// expiresAt is a date and time in the future, implementations shall delete
703 : /// the Registration when this point in time is reached." The sweep is lazy
704 : /// — "final deletion will always lag the expiresAt timestamp" — so the write
705 : /// that names an expired registration performs the deletion and then sees
706 : /// what every later operation sees, which is what lets a create take the id
707 : /// back: 5.9.2.4 raises AlreadyExists only for a registration that exists,
708 : /// and the store's create is the atomic check-and-insert, so the row has to
709 : /// be gone before it runs. Only writes call this. The read handlers filter
710 : /// on `reg_expired` and touch nothing; the row they hide is freed by
711 : /// `sweep_expired_registrations` on the sweep tick. Unlike a Subscription
712 : /// (5.8.6), a Registration has no `status` member that keeps an expired one
713 : /// visible.
714 3596 : async fn take_live_registration(
715 3596 : st: &AppState,
716 3596 : tenant: &TenantId,
717 3596 : id: &str,
718 3596 : ) -> Result<Option<Value>, NgsiError> {
719 3596 : match st.store.get(tenant, Kind::Registration, id).await? {
720 482 : Some(doc) if reg_expired(&doc) => {
721 6 : st.store.delete(tenant, Kind::Registration, id).await?;
722 6 : Ok(None)
723 : }
724 3590 : live => Ok(live),
725 : }
726 3596 : }
727 :
728 : /// 4.22 for registrations: the read paths refuse an expired registration
729 : /// (`reg_expired`), and this is what removes it. Without a collector the
730 : /// rows a read already hides stay for the life of the broker, so the
731 : /// predicate that hides one is the predicate that reaps it.
732 : ///
733 : /// A tenant the driver refuses to enumerate reaps nothing rather than
734 : /// failing the tick: the sweep is opportunistic, and every read still
735 : /// hides what it leaves behind.
736 20050 : pub(crate) async fn sweep_expired_registrations(st: &AppState, tenant: &TenantId) -> usize {
737 20050 : let mut dead: Vec<String> = Vec::new();
738 171860 : if walk_docs(st, tenant, Kind::Registration, |doc| {
739 170441 : if reg_expired(&doc) {
740 8 : if let Some(id) = doc.get("id").and_then(Value::as_str) {
741 8 : dead.push(id.to_owned());
742 8 : }
743 170433 : }
744 170441 : Ok(())
745 170441 : })
746 20050 : .await
747 20050 : .is_err()
748 : {
749 0 : return 0;
750 20050 : }
751 20050 : let mut n = 0;
752 20050 : for id in dead {
753 8 : if st
754 8 : .store
755 8 : .delete(tenant, Kind::Registration, &id)
756 8 : .await
757 8 : .unwrap_or(false)
758 8 : {
759 8 : n += 1;
760 8 : }
761 : }
762 20050 : n
763 20050 : }
764 :
765 : /// 4.3.6.3 Proxied Registrations: "An exclusive registration shall always
766 : /// relate to specific Attributes found on a single Entity. Thus, the
767 : /// registration shall define both: an entity id (i.e. an id pattern or Entity
768 : /// type defining a group of entities is not supported for exclusive
769 : /// registrations) `[and]` Attributes."
770 3448 : pub fn validate_exclusive(doc: &Map<String, Value>) -> Result<(), NgsiError> {
771 3448 : if doc.get("mode").and_then(Value::as_str) != Some("exclusive") {
772 2192 : return Ok(());
773 1256 : }
774 1256 : let bad = |m: &str| NgsiError::BadRequestData(format!("{m} (4.3.6.3)"));
775 1256 : let infos = doc
776 1256 : .get("information")
777 1256 : .and_then(Value::as_array)
778 1256 : .map(Vec::as_slice)
779 1256 : .unwrap_or_default();
780 1256 : for info in infos {
781 1260 : let has_attrs = ["propertyNames", "relationshipNames"].iter().any(|k| {
782 1260 : info.get(*k)
783 1260 : .and_then(Value::as_array)
784 1260 : .is_some_and(|a| !a.is_empty())
785 1260 : });
786 1256 : if !has_attrs {
787 4 : return Err(bad("an exclusive registration shall define Attributes"));
788 1252 : }
789 1252 : let ids_only = info
790 1252 : .get("entities")
791 1252 : .and_then(Value::as_array)
792 1252 : .is_some_and(|es| {
793 1252 : es.iter().all(|e| {
794 1252 : e.get("id").and_then(Value::as_str).is_some() && e.get("idPattern").is_none()
795 1252 : })
796 1252 : });
797 1252 : if !ids_only {
798 8 : return Err(bad(
799 8 : "an exclusive registration shall name an entity id — an id pattern or \
800 8 : Entity type defining a group of entities is not supported",
801 8 : ));
802 1244 : }
803 : }
804 1244 : Ok(())
805 3448 : }
806 :
807 : /// 4.3.6.3: "Once an exclusive Context Source Registration has been created,
808 : /// no further exclusive or redirect Context Source Registrations can be
809 : /// created for that same combination of Entity ID and Attributes" — and per
810 : /// 5.9.2, registering an exclusive Context Source when "an exclusive or
811 : /// redirect Context Source Registration already matches against the Entity ID
812 : /// (URI) and any of the Attributes defined in the registration" raises a
813 : /// Conflict (409; Table 6.3.2-1 defines no Conflict type, so it travels as
814 : /// AlreadyExists — the project's standing 409 mapping). Redirect overlapping
815 : /// redirect stays legal: "operations are distributed to all registered
816 : /// Context Sources".
817 3100 : pub async fn check_proxied_overlap(
818 3100 : st: &AppState,
819 3100 : tenant: &antares_model::TenantId,
820 3100 : doc: &Map<String, Value>,
821 3100 : self_id: Option<&str>,
822 3100 : ctx: &Context,
823 3100 : ) -> Result<(), NgsiError> {
824 3100 : let mode = doc
825 3100 : .get("mode")
826 3100 : .and_then(Value::as_str)
827 3100 : .unwrap_or("inclusive");
828 3100 : if mode != "exclusive" && mode != "redirect" {
829 1752 : return Ok(());
830 1348 : }
831 1348 : let infos = doc
832 1348 : .get("information")
833 1348 : .and_then(Value::as_array)
834 1348 : .map(Vec::as_slice)
835 1348 : .unwrap_or_default();
836 : // Fail closed: treating a lookup failure as "no conflicts" would admit a
837 : // second exclusive registration for the same scope.
838 2134 : walk_docs(st, tenant, Kind::Registration, |other| {
839 2134 : let other = &other;
840 2134 : if other.get("id").and_then(Value::as_str) == self_id {
841 328 : return Ok(());
842 1806 : }
843 1806 : if reg_expired(other) {
844 0 : return Ok(());
845 1806 : }
846 1806 : let omode = other
847 1806 : .get("mode")
848 1806 : .and_then(Value::as_str)
849 1806 : .unwrap_or("inclusive");
850 : // new exclusive × existing proxied; new redirect × existing exclusive
851 1806 : let guarded = match mode {
852 1806 : "exclusive" => omode == "exclusive" || omode == "redirect",
853 20 : _ => omode == "exclusive",
854 : };
855 1806 : if !guarded {
856 784 : return Ok(());
857 1022 : }
858 1022 : for info in infos {
859 1022 : let attrs: Vec<String> = ["propertyNames", "relationshipNames"]
860 1022 : .iter()
861 2044 : .filter_map(|k| info.get(*k).and_then(Value::as_array))
862 1022 : .flatten()
863 1022 : .filter_map(Value::as_str)
864 1022 : .map(str::to_owned)
865 1022 : .collect();
866 1022 : let empty = Vec::new();
867 1022 : let entities = info
868 1022 : .get("entities")
869 1022 : .and_then(Value::as_array)
870 1022 : .unwrap_or(&empty);
871 1022 : let ids: Vec<String> = entities
872 1022 : .iter()
873 1022 : .filter_map(|e| e.get("id").and_then(Value::as_str))
874 1022 : .map(str::to_owned)
875 1022 : .collect();
876 1022 : let types: Vec<String> = entities
877 1022 : .iter()
878 1022 : .flat_map(ei_types)
879 1022 : .map(str::to_owned)
880 1022 : .collect();
881 1022 : let spec = CsrSpec {
882 1022 : types: (!types.is_empty()).then_some(types),
883 1022 : ids: (!ids.is_empty()).then_some(ids),
884 1022 : id_pattern: entities
885 1022 : .iter()
886 1022 : .find_map(|e| e.get("idPattern").and_then(Value::as_str))
887 1022 : .map(str::to_owned),
888 1022 : attrs: (!attrs.is_empty()).then_some(attrs),
889 1022 : dataset_ids: None,
890 1022 : csf: None,
891 1022 : geo: None,
892 1022 : temporal: None,
893 : };
894 1022 : if csr_matches(&spec, other, ctx) {
895 1010 : let oid = other.get("id").and_then(Value::as_str).unwrap_or("?");
896 1010 : return Err(NgsiError::AlreadyExists(format!(
897 1010 : "proxied registration overlaps {oid} for the same combination of \
898 1010 : Entity ID and Attributes (4.3.6.3)"
899 1010 : )));
900 12 : }
901 : }
902 12 : Ok(())
903 2134 : })
904 1348 : .await
905 3100 : }
906 :
907 : /// The 5.9.2.4 conflict rules ("if an exclusive or redirect Context Source
908 : /// Registration already matches … an error of type Conflict shall be
909 : /// raised") are decided by reading the registration set, and the create or
910 : /// update that follows writes it. Read and write are separate store
911 : /// operations, so without this lock two requests can each observe a
912 : /// conflict-free set and both land, leaving the two exclusive registrations
913 : /// for one Entity ID and Attribute the clause forbids. Every registration
914 : /// write holds it for the whole check-then-write sequence.
915 : ///
916 : /// One lock per tenant: the clause decides a conflict against the
917 : /// registrations of the tenant being written, and two tenants' sets are
918 : /// disjoint, so the overlap scan of one tenant's idPattern-only
919 : /// registration never stalls another tenant's write. The map only grows,
920 : /// one entry per tenant that ever wrote a registration.
921 : ///
922 : /// A `tokio::sync::Mutex`, not a `std` one: the guarded section awaits the
923 : /// store between the check and the write, and a `std` guard cannot be held
924 : /// across an await.
925 : ///
926 : /// ponytail: process-local, so two broker pods sharing one database do not
927 : /// exclude each other and the check-then-write races between them (the gap
928 : /// the 5.9.2.4 ledger entry names). A database lock held across the section
929 : /// is the upgrade, and it pins a connection for the whole overlap scan —
930 : /// holders that each still need a connection to run that scan exhaust the
931 : /// pool and wait on each other, so the upgrade owes a bound on how many may
932 : /// hold at once.
933 : static REGISTRATION_WRITE: std::sync::Mutex<
934 : std::collections::BTreeMap<String, std::sync::Arc<tokio::sync::Mutex<()>>>,
935 : > = std::sync::Mutex::new(std::collections::BTreeMap::new());
936 :
937 3124 : async fn registration_write_lock(tenant: &TenantId) -> tokio::sync::OwnedMutexGuard<()> {
938 3124 : let lock = REGISTRATION_WRITE
939 3124 : .lock()
940 3124 : .unwrap_or_else(std::sync::PoisonError::into_inner)
941 3124 : .entry(tenant.as_str().to_owned())
942 3124 : .or_default()
943 3124 : .clone();
944 3124 : lock.lock_owned().await
945 3120 : }
946 :
947 : // ---------- handlers ----------
948 :
949 3444 : pub async fn create_registration(
950 3444 : State(st): State<AppState>,
951 3444 : CleanParams(params): CleanParams,
952 3444 : headers: HeaderMap,
953 3444 : body: Bytes,
954 3444 : ) -> Response {
955 3444 : let go = async {
956 3444 : let tenant = tenant_from(&headers)?;
957 3444 : check_params(¶ms, &["local"])?;
958 3444 : let parsed = parse_body(&st.loader, &headers, &body, BodyKind::Standard).await?;
959 2876 : let obj = parsed.value.as_object().ok_or_else(|| {
960 0 : NgsiError::BadRequestData("registration must be a JSON object".into())
961 0 : })?;
962 : // ADR-0020: 5.9.2 lets the client choose the registration id, and
963 : // it is in hand here — see the same note on subscription create.
964 2876 : let named = obj.get("id").and_then(Value::as_str);
965 2876 : gate!(st, &tenant, &headers, "5.9.2", ids: named.as_slice()).await?;
966 2876 : let mut norm = normalize_registration(obj, &parsed.ctx, false)?;
967 2680 : let id = match norm.get("id").and_then(Value::as_str) {
968 2658 : Some(id) => id.to_owned(),
969 : None => {
970 22 : let id = format!(
971 : "urn:ngsi-ld:ContextSourceRegistration:{}",
972 22 : uuid::Uuid::new_v4()
973 : );
974 22 : norm.insert("id".into(), Value::String(id.clone()));
975 22 : id
976 : }
977 : };
978 2680 : validate_auxiliary_ops(&norm)?;
979 1966 : let doc = {
980 2676 : let _serialized = registration_write_lock(&tenant).await;
981 2676 : take_live_registration(&st, &tenant, &id).await?;
982 2676 : check_entity_conflict(&st, &tenant, &norm).await?;
983 2668 : check_proxied_overlap(&st, &tenant, &norm, None, &parsed.ctx).await?;
984 1966 : let ts = now_iso();
985 1966 : norm.insert("createdAt".into(), Value::String(ts.clone()));
986 1966 : norm.insert("modifiedAt".into(), Value::String(ts));
987 1966 : let doc = Value::Object(norm);
988 1966 : if !st
989 1966 : .store
990 1966 : .create(&tenant, Kind::Registration, &id, doc.clone())
991 1966 : .await?
992 : {
993 0 : return Err(
994 0 : NgsiError::AlreadyExists(format!("registration {id} already exists")).into(),
995 0 : );
996 1966 : }
997 1966 : doc
998 : };
999 1966 : st.reg_changed(&tenant, &id, Some(&doc));
1000 1966 : crate::notify::csource_fanout(&st, &tenant, None, Some(doc)).await;
1001 1966 : Ok::<_, ApiError>(created(
1002 1966 : format!("/ngsi-ld/v1/csourceRegistrations/{id}"),
1003 1966 : &tenant,
1004 1966 : ))
1005 3444 : };
1006 3444 : go.await.unwrap_or_else(|e| e.into_response())
1007 3444 : }
1008 :
1009 102 : pub async fn retrieve_registration(
1010 102 : State(st): State<AppState>,
1011 102 : Path(id): Path<String>,
1012 102 : CleanParams(params): CleanParams,
1013 102 : headers: HeaderMap,
1014 102 : ) -> Response {
1015 102 : let go = async {
1016 102 : let tenant = tenant_from(&headers)?;
1017 102 : antares_model::EntityId::new(&id)
1018 102 : .map_err(|_| NgsiError::BadRequestData(format!("invalid registration id {id:?}")))?;
1019 98 : check_params(¶ms, &["options", "format", "local"])?;
1020 98 : let accept = parse_accept(&headers)?;
1021 98 : let ctx = request_context(&st.loader, &headers).await?;
1022 94 : gate!(st, &tenant, &headers, "5.10.1", ids: &[&id]).await?;
1023 94 : let doc = st
1024 94 : .store
1025 94 : .get(&tenant, Kind::Registration, &id)
1026 94 : .await?
1027 94 : .filter(|d| !reg_expired(d))
1028 94 : .ok_or_else(|| NgsiError::ResourceNotFound(format!("registration {id} not found")))?;
1029 64 : let sys = sys_attrs_asked(¶ms);
1030 64 : Ok::<_, ApiError>(respond(
1031 64 : StatusCode::OK,
1032 64 : present_registration(&doc, &ctx, sys),
1033 64 : &ctx,
1034 64 : accept,
1035 64 : &tenant,
1036 64 : ))
1037 102 : };
1038 102 : go.await.unwrap_or_else(|e| e.into_response())
1039 102 : }
1040 :
1041 124 : pub async fn query_registrations(
1042 124 : State(st): State<AppState>,
1043 124 : CleanParams(params): CleanParams,
1044 124 : headers: HeaderMap,
1045 124 : ) -> Response {
1046 124 : let go = async {
1047 124 : let tenant = tenant_from(&headers)?;
1048 124 : check_params(
1049 124 : ¶ms,
1050 124 : &[
1051 124 : "id",
1052 124 : "idPattern",
1053 124 : "type",
1054 124 : "attrs",
1055 124 : "q",
1056 124 : "georel",
1057 124 : "geometry",
1058 124 : "coordinates",
1059 124 : "geoproperty",
1060 124 : "timeproperty",
1061 124 : "timerel",
1062 124 : "timeAt",
1063 124 : "endTimeAt",
1064 124 : "csf",
1065 124 : "limit",
1066 124 : "offset",
1067 124 : "count",
1068 124 : "options",
1069 124 : "format",
1070 124 : "local",
1071 124 : "scopeQ",
1072 124 : ],
1073 0 : )?;
1074 124 : let accept = parse_accept(&headers)?;
1075 116 : let ctx = request_context(&st.loader, &headers).await?;
1076 96 : gate!(
1077 : st, &tenant, &headers, "5.10.2",
1078 : scope_q: params.get("scopeQ").map(String::as_str),
1079 : )
1080 96 : .await?;
1081 96 : let bad = NgsiError::BadRequestData;
1082 96 : let mut spec = CsrSpec::default();
1083 96 : if let Some(s) = params.get("id") {
1084 0 : let mut ids = Vec::new();
1085 0 : for i in s.split(',') {
1086 0 : antares_model::EntityId::new(i)
1087 0 : .map_err(|_| bad(format!("invalid id in list: {i:?}")))?;
1088 0 : ids.push(i.to_owned());
1089 : }
1090 0 : spec.ids = Some(ids);
1091 96 : }
1092 96 : spec.id_pattern = params
1093 96 : .get("idPattern")
1094 96 : .map(|p| {
1095 0 : antares_ql::regex::compile(p)
1096 0 : .map(|_| p.clone())
1097 0 : .map_err(|_| bad(format!("invalid idPattern {p:?}")))
1098 0 : })
1099 96 : .transpose()?;
1100 : // Table 6.8.3.2-1: `type` is a "Selection of Entity Types as per
1101 : // clause 4.17", i.e. ONE expression — splitting it on ',' and
1102 : // expanding the fragments mangles every selector that uses ';' or
1103 : // parentheses. entity_info_matches evaluates it whole.
1104 96 : spec.types = params.get("type").cloned().map(|s| vec![s]);
1105 96 : let mut attrs: Vec<String> = params
1106 96 : .get("attrs")
1107 96 : .map(|s| s.split(',').map(|t| ctx.expand_key(t.trim())).collect())
1108 96 : .unwrap_or_default();
1109 : // attributes referenced in q / geoQ count as query projection
1110 : // attributes for matching (5.10.2.4)
1111 : // `CleanParams` percent-decodes every value once, at the extractor
1112 : // (6.3.1). Decoding again here reads escapes that are part of the
1113 : // value, so a `q` legitimately containing `%22` became one carrying a
1114 : // bare quote and 4.9's parser refused a legal query. `csf` below
1115 : // takes the extractor's form, and so does this.
1116 96 : if let Some(q) = params.get("q") {
1117 2 : let ast = antares_ql::parse_q(q)?;
1118 2 : let mut roots = Vec::new();
1119 2 : q_attr_roots(&ast, &mut roots);
1120 2 : attrs.extend(roots.into_iter().map(|r| ctx.expand_key(&r)));
1121 94 : }
1122 96 : let geo = antares_ql::geo::GeoQuery::from_params(¶ms)?;
1123 96 : if let Some(g) = &geo {
1124 4 : attrs.push(ctx.expand_key(&g.geoproperty));
1125 92 : }
1126 96 : if !attrs.is_empty() {
1127 6 : spec.attrs = Some(attrs);
1128 90 : }
1129 : // 5.10.2.4: a discriminating input is required, else too wide
1130 : // (the suite additionally accepts id-only queries — 037_10_01)
1131 96 : if spec.types.is_none()
1132 42 : && spec.attrs.is_none()
1133 42 : && spec.ids.is_none()
1134 42 : && spec.id_pattern.is_none()
1135 : {
1136 42 : return Err(bad(
1137 42 : "query too wide: one of type, attrs, q or geo query is required (5.10.2.4)".into(),
1138 42 : )
1139 42 : .into());
1140 54 : }
1141 : // 5.10.2.4: csf is a 4.9 query over Context Source Properties
1142 54 : let csf = params
1143 54 : .get("csf")
1144 54 : .map(|c| antares_ql::parse_q(c))
1145 54 : .transpose()?;
1146 54 : let scope_q = params.get("scopeQ").cloned();
1147 : // temporal query: validate + interval presence rules (5.10.2.4)
1148 54 : let temporal = crate::temporalq::TemporalQ::from_params(¶ms, false)?
1149 54 : .filter(|t| t.timerel != "any");
1150 : // 5.10.2.4 fixes the order: run the query returning the
1151 : // registrations that "meet all the applicable conditions", THEN
1152 : // "Pagination logic shall be in place as mandated by clause 5.5.9".
1153 : // Filter first, page second — so the window 5.8.4 pushes into the
1154 : // store cannot be pushed here, where it would cut the page out of
1155 : // the stored rows and serve registrations the query never matched.
1156 : //
1157 : // What CAN be bounded is the read. Walking the tenant in pages keeps
1158 : // the peak at one page plus the matches, and moves the ceiling from
1159 : // what the tenant STORES to what the query MATCHES; 5.5.6 licenses
1160 : // TooManyResults for "a query operation ... producing so many
1161 : // results that can potentially exhaust client or server resources",
1162 : // which is a statement about the result and not about the store.
1163 237 : let keep = |doc: &Value| {
1164 237 : if reg_expired(doc) {
1165 5 : return false;
1166 232 : }
1167 232 : let has_interval =
1168 232 : doc.get("observationInterval").is_some() || doc.get("managementInterval").is_some();
1169 232 : match &temporal {
1170 0 : None if has_interval => return false,
1171 0 : Some(tq) if !temporal_interval_matches(doc, tq) => return false,
1172 232 : _ => {}
1173 : }
1174 : // 5.10.2.4: csf vs Context Source Properties, Scope query vs
1175 : // the registration scope, geoquery vs its location
1176 232 : if let Some(csf) = &csf {
1177 12 : if !csf_matches(csf, doc, &ctx) {
1178 8 : return false;
1179 4 : }
1180 220 : }
1181 224 : if let Some(sq) = &scope_q {
1182 8 : if !crate::scope_matches(sq, doc) {
1183 4 : return false;
1184 4 : }
1185 216 : }
1186 220 : if let Some(g) = &geo {
1187 8 : match doc.get("location") {
1188 4 : Some(geom) if g.matches_geometry(geom) => {}
1189 4 : _ => return false,
1190 : }
1191 212 : }
1192 216 : csr_matches(&spec, doc, &ctx)
1193 237 : };
1194 54 : let matches = collect_matching(&st, &tenant, keep, *crate::bounds::MAX_FOLD_DOCS).await?;
1195 54 : let (page, count_hdr, links) = crate::paging::paginate_accept(
1196 54 : &st,
1197 54 : ¶ms,
1198 54 : matches,
1199 54 : "/ngsi-ld/v1/csourceRegistrations",
1200 54 : accept,
1201 0 : )?;
1202 54 : let sys = sys_attrs_asked(¶ms);
1203 54 : let payload: Vec<Value> = page
1204 54 : .iter()
1205 62 : .map(|d| present_registration(d, &ctx, sys))
1206 54 : .collect();
1207 54 : let mut resp =
1208 54 : crate::negotiate::respond_list(StatusCode::OK, payload, &ctx, accept, &tenant);
1209 54 : attach_paging(&mut resp, count_hdr, &links);
1210 54 : Ok::<_, ApiError>(resp)
1211 124 : };
1212 124 : go.await.unwrap_or_else(|e| e.into_response())
1213 124 : }
1214 :
1215 : /// Registrations per page of the 5.10.2.4 walk: the peak transient
1216 : /// allocation on top of the match set.
1217 : const SCAN_PAGE: usize = 1_000;
1218 :
1219 : /// Visit every document of one kind in one tenant, a page at a time. An
1220 : /// `Err` from `visit` ends the walk and is the walk's own result, which is
1221 : /// how the 5.9.2.4 conflict checks stop at the document they conflict with.
1222 : ///
1223 : /// The whole-tenant `list` and `query_entities` carry the row ceiling meant
1224 : /// for client queries (5.5.6). Every caller below must see EVERY document —
1225 : /// to answer a query over the registrations, to refuse a second exclusive
1226 : /// registration for the same scope, or to find the Entity a redirect
1227 : /// registration would shadow — so that ceiling refused them outright once a
1228 : /// tenant held more than it, whatever the read narrowed to and however few
1229 : /// conflicts existed. A page bounds the allocation by construction and
1230 : /// carries no ceiling, so a large tenant costs time here rather than a
1231 : /// permanent 403.
1232 61738 : pub(crate) async fn walk_docs(
1233 61738 : st: &AppState,
1234 61738 : tenant: &antares_model::TenantId,
1235 61738 : kind: Kind,
1236 61738 : mut visit: impl FnMut(Value) -> Result<(), NgsiError>,
1237 61738 : ) -> Result<(), NgsiError> {
1238 61738 : let mut after: Option<String> = None;
1239 : loop {
1240 61742 : let page = st
1241 61742 : .store
1242 61742 : .list_page(tenant, kind, after.as_deref(), SCAN_PAGE)
1243 61742 : .await?;
1244 61742 : let short = page.len() < SCAN_PAGE;
1245 61742 : let before = after.clone();
1246 179566 : for doc in page {
1247 177797 : if let Some(id) = doc.get("id").and_then(Value::as_str) {
1248 177781 : after = Some(id.to_owned());
1249 177781 : }
1250 177797 : visit(doc)?;
1251 : }
1252 : // A short page ends the walk, and so does a cursor that did not
1253 : // move: only a document carrying an `id` advances it, so a full page
1254 : // without one would otherwise be re-read forever.
1255 60708 : if short || after == before {
1256 60704 : break;
1257 4 : }
1258 : }
1259 60704 : Ok(())
1260 61738 : }
1261 :
1262 : /// Every registration of one tenant that `keep` accepts.
1263 : /// Every registration of one tenant that `keep` accepts, up to `ceiling`.
1264 : ///
1265 : /// 5.10.2.4 filters before it pages, so the page cannot be pushed into the
1266 : /// store and the whole match set is held at once. A broker is built for
1267 : /// 100 000+ registrations per tenant, so "the whole match set" is a number a
1268 : /// client picks with one `type=` — and 5.5.6 gives the answer for "a query
1269 : /// operation … producing so many results that can potentially exhaust client
1270 : /// or server resources": TooManyResults, rather than the memory.
1271 66 : async fn collect_matching(
1272 66 : st: &AppState,
1273 66 : tenant: &antares_model::TenantId,
1274 66 : keep: impl Fn(&Value) -> bool,
1275 66 : ceiling: usize,
1276 66 : ) -> Result<Vec<Value>, NgsiError> {
1277 66 : let mut matches = Vec::new();
1278 289 : walk_docs(st, tenant, Kind::Registration, |doc| {
1279 289 : if keep(&doc) {
1280 150 : if matches.len() == ceiling {
1281 4 : return Err(NgsiError::TooManyResults(format!(
1282 4 : "the query matches more than {ceiling} registrations — narrow it (5.5.6)"
1283 4 : )));
1284 146 : }
1285 146 : matches.push(doc);
1286 139 : }
1287 285 : Ok(())
1288 289 : })
1289 66 : .await?;
1290 62 : Ok(matches)
1291 66 : }
1292 :
1293 : /// Root attribute names referenced by a q= expression (5.10.2.4: they count
1294 : /// as query projection attributes for RegistrationInfo matching).
1295 2 : fn q_attr_roots(node: &antares_ql::QNode, out: &mut Vec<String>) {
1296 : use antares_ql::QNode::*;
1297 2 : match node {
1298 0 : And(v) | Or(v) => v.iter().for_each(|n| q_attr_roots(n, out)),
1299 2 : Cmp { path, .. } | Exists { path, .. } => {
1300 2 : if let Some(r) = path.top() {
1301 2 : out.push(r.to_owned());
1302 2 : }
1303 : }
1304 : }
1305 2 : }
1306 :
1307 : /// 5.9.3 Update Context Source Registration: invalid URI 400, unknown 404,
1308 : /// 5.2.9 fragment merge per 5.5.8 with every mode rule re-checked on the
1309 : /// post-merge document (4.3.6.3 exclusive shape, auxiliary ops limit,
1310 : /// entity/registration conflicts).
1311 436 : pub async fn update_registration(
1312 436 : State(st): State<AppState>,
1313 436 : Path(id): Path<String>,
1314 436 : CleanParams(params): CleanParams,
1315 436 : headers: HeaderMap,
1316 436 : body: Bytes,
1317 436 : ) -> Response {
1318 436 : let go = async {
1319 436 : let tenant = tenant_from(&headers)?;
1320 436 : antares_model::EntityId::new(&id)
1321 436 : .map_err(|_| NgsiError::BadRequestData(format!("invalid registration id {id:?}")))?;
1322 436 : check_params(¶ms, &["local"])?;
1323 436 : let parsed = parse_body(&st.loader, &headers, &body, BodyKind::MergePatch).await?;
1324 432 : let obj = parsed.object(NgsiError::BadRequestData(
1325 432 : "fragment must be a JSON object".into(),
1326 432 : ))?;
1327 432 : gate!(st, &tenant, &headers, "5.9.3", ids: &[&id]).await?;
1328 432 : let norm = normalize_registration(obj, &parsed.ctx, true)?;
1329 432 : let ts = now_iso();
1330 : // The 5.9.3.4 re-checks below read the registration set that the
1331 : // mutate then writes — the pair is atomic or a concurrent write can
1332 : // invalidate the checks between them.
1333 124 : let (before, res) = {
1334 432 : let _serialized = registration_write_lock(&tenant).await;
1335 432 : let before = take_live_registration(&st, &tenant, &id).await?;
1336 432 : if let Some(prev) = before.as_ref().and_then(Value::as_object) {
1337 : // validate the post-merge document (4.3.6.3) BEFORE mutating:
1338 : // a patch may flip the mode or rewrite information
1339 420 : let mut merged = prev.clone();
1340 420 : for (k, v) in &norm {
1341 416 : if k == "id" {
1342 0 : continue;
1343 416 : }
1344 416 : if v.is_null() {
1345 0 : merged.remove(k);
1346 416 : } else {
1347 416 : merged.insert(k.clone(), v.clone());
1348 416 : }
1349 : }
1350 420 : validate_exclusive(&merged)?;
1351 : // 5.9.3.4: the mode-specific rules apply to the merged document
1352 420 : validate_auxiliary_ops(&merged)?;
1353 416 : check_entity_conflict(&st, &tenant, &merged).await?;
1354 412 : check_proxied_overlap(&st, &tenant, &merged, Some(&id), &parsed.ctx).await?;
1355 12 : }
1356 124 : let res = st
1357 124 : .store
1358 124 : .mutate(&tenant, Kind::Registration, &id, |doc| {
1359 112 : let Some(target) = doc.as_object_mut() else {
1360 0 : return Err(NgsiError::InternalError(
1361 0 : "stored registration is not a JSON object".into(),
1362 0 : ));
1363 : };
1364 112 : crate::apply_doc_fragment(target, &norm, &ts);
1365 112 : Ok::<(), NgsiError>(())
1366 112 : })
1367 124 : .await?;
1368 124 : (before, res)
1369 : };
1370 112 : match res {
1371 12 : None => Err(NgsiError::ResourceNotFound(format!("registration {id} not found")).into()),
1372 0 : Some(Err(e)) => Err(ApiError::from(e)),
1373 : Some(Ok(())) => {
1374 112 : let after = st.store.get(&tenant, Kind::Registration, &id).await?;
1375 112 : st.reg_changed(&tenant, &id, after.as_ref());
1376 112 : crate::notify::csource_fanout(&st, &tenant, before, after).await;
1377 112 : Ok(no_content(&tenant))
1378 : }
1379 : }
1380 436 : };
1381 436 : go.await.unwrap_or_else(|e| e.into_response())
1382 436 : }
1383 :
1384 : /// 5.9.4 Delete Context Source Registration: invalid URI 400, unknown id
1385 : /// 404, 204 on removal (registry mirror + csource subscriptions refresh).
1386 488 : pub async fn delete_registration(
1387 488 : State(st): State<AppState>,
1388 488 : Path(id): Path<String>,
1389 488 : CleanParams(params): CleanParams,
1390 488 : headers: HeaderMap,
1391 488 : ) -> Response {
1392 488 : let go = async {
1393 488 : let tenant = tenant_from(&headers)?;
1394 488 : antares_model::EntityId::new(&id)
1395 488 : .map_err(|_| NgsiError::BadRequestData(format!("invalid registration id {id:?}")))?;
1396 488 : check_params(¶ms, &["local"])?;
1397 488 : gate!(st, &tenant, &headers, "5.9.4", ids: &[&id]).await?;
1398 488 : let before = take_live_registration(&st, &tenant, &id).await?;
1399 488 : if st.store.delete(&tenant, Kind::Registration, &id).await? {
1400 56 : st.reg_changed(&tenant, &id, None);
1401 56 : crate::notify::csource_fanout(&st, &tenant, before, None).await;
1402 56 : Ok(no_content(&tenant))
1403 : } else {
1404 432 : Err::<Response, ApiError>(
1405 432 : NgsiError::ResourceNotFound(format!("registration {id} not found")).into(),
1406 432 : )
1407 : }
1408 488 : };
1409 488 : go.await.unwrap_or_else(|e| e.into_response())
1410 488 : }
1411 :
1412 : #[cfg(test)]
1413 : mod clause_4_20 {
1414 : use super::*;
1415 : use antares_jsonld::Loader;
1416 : use serde_json::json;
1417 :
1418 228 : fn reg_with(ops: Value) -> Result<Map<String, Value>, NgsiError> {
1419 228 : let ctx = Loader::new().core();
1420 228 : let doc = json!({
1421 228 : "id": "urn:ngsi-ld:ContextSourceRegistration:ops",
1422 228 : "type": "ContextSourceRegistration",
1423 228 : "endpoint": "http://peer:9090",
1424 228 : "information": [{"entities": [{"type": "Building"}]}],
1425 228 : "operations": ops
1426 : });
1427 228 : normalize_registration(doc.as_object().expect("object"), &ctx, false)
1428 228 : }
1429 :
1430 : /// Table 5.2.9-1: `operations` entries "are limited to the named API
1431 : /// operations and named operation groups (see clause 4.20)". Every name
1432 : /// of Table 4.20-1 and every group of Table 4.20-2 is accepted, and
1433 : /// nothing else is — including a name that only looks like one.
1434 : #[test]
1435 4 : fn only_the_4_20_vocabulary_is_accepted() {
1436 192 : for op in OPERATION_NAMES.iter().chain(OPERATION_GROUPS) {
1437 192 : assert!(reg_with(json!([op])).is_ok(), "{op} is a 4.20 name");
1438 : }
1439 4 : assert!(
1440 4 : reg_with(json!(OPERATION_NAMES.to_vec())).is_ok(),
1441 : "the whole vocabulary at once is legal"
1442 : );
1443 24 : for bad in [
1444 4 : "notARealOp",
1445 4 : "createentity",
1446 4 : "createEntity ",
1447 4 : "federationops",
1448 4 : "queryEntities",
1449 4 : "",
1450 4 : ] {
1451 24 : let e = reg_with(json!([bad])).expect_err("outside the vocabulary");
1452 24 : assert!(
1453 24 : matches!(e, NgsiError::BadRequestData(_)),
1454 : "{bad} must be BadRequestData, got {e:?}"
1455 : );
1456 : }
1457 : // 5.2.9: the member is present-or-absent, never an empty list
1458 4 : assert!(reg_with(json!([])).is_err(), "empty operations");
1459 4 : assert!(reg_with(json!("federationOps")).is_err(), "not an array");
1460 4 : }
1461 : }
1462 :
1463 : #[cfg(test)]
1464 : mod csi_tests {
1465 : use super::*;
1466 : use antares_jsonld::Loader;
1467 : use serde_json::json;
1468 : use std::collections::HashMap;
1469 :
1470 : /// 5.5.4: "urn:ngsi-ld:null" as a first-level member value is
1471 : /// BadRequestData on create; on patch it is the Fragment removal form.
1472 : #[test]
1473 4 : fn clause_5_5_4_first_level_null_in_registration() {
1474 4 : let ctx = Loader::new().core();
1475 4 : let doc = json!({
1476 4 : "id": "urn:ngsi-ld:ContextSourceRegistration:n1",
1477 4 : "type": "ContextSourceRegistration",
1478 4 : "endpoint": "http://peer:9090",
1479 4 : "description": "urn:ngsi-ld:null",
1480 4 : "information": [{"entities": [{"type": "Building"}]}]
1481 : });
1482 4 : assert!(
1483 4 : normalize_registration(doc.as_object().unwrap(), &ctx, false).is_err(),
1484 : "create with a first-level null URN must be rejected"
1485 : );
1486 : // patch: the same member is a removal fragment (stored as Null)
1487 4 : let patch = json!({"description": "urn:ngsi-ld:null"});
1488 4 : let out = normalize_registration(patch.as_object().unwrap(), &ctx, true)
1489 4 : .expect("patch fragment null is legal");
1490 4 : assert!(out["description"].is_null());
1491 4 : }
1492 :
1493 : /// 4.3.6.3: "the registration shall define both: an entity id (i.e. an id
1494 : /// pattern or Entity type defining a group of entities is not supported
1495 : /// for exclusive registrations) [and] Attributes."
1496 : #[tokio::test]
1497 4 : async fn exclusive_registration_requires_entity_id_and_attributes() {
1498 4 : let ctx = Loader::new().core();
1499 24 : let mk = |mode: &str, info: Value| {
1500 24 : json!({
1501 24 : "id": "urn:ngsi-ld:ContextSourceRegistration:x1",
1502 24 : "type": "ContextSourceRegistration",
1503 24 : "endpoint": "http://peer:9090",
1504 24 : "mode": mode,
1505 24 : "information": [info]
1506 : })
1507 24 : };
1508 24 : let norm = |mode: &str, info: Value| {
1509 24 : normalize_registration(mk(mode, info).as_object().unwrap(), &ctx, false)
1510 24 : };
1511 4 : let full = json!({
1512 4 : "entities": [{"id": "urn:ngsi-ld:Vehicle:v1", "type": "Vehicle"}],
1513 4 : "propertyNames": ["speed"]
1514 : });
1515 4 : assert!(norm("exclusive", full.clone()).is_ok());
1516 4 : assert!(
1517 4 : norm(
1518 4 : "exclusive",
1519 4 : json!({"entities": [{"id": "urn:ngsi-ld:Vehicle:v1", "type": "Vehicle"}]})
1520 4 : )
1521 4 : .is_err(),
1522 : "exclusive without Attributes"
1523 : );
1524 4 : assert!(
1525 4 : norm(
1526 4 : "exclusive",
1527 4 : json!({"entities": [{"type": "Vehicle"}], "propertyNames": ["speed"]})
1528 4 : )
1529 4 : .is_err(),
1530 : "exclusive with a type-only entity group"
1531 : );
1532 4 : assert!(
1533 4 : norm(
1534 4 : "exclusive",
1535 4 : json!({"entities": [{"idPattern": ".*", "type": "Vehicle"}],
1536 4 : "propertyNames": ["speed"]})
1537 4 : )
1538 4 : .is_err(),
1539 : "exclusive with an id pattern"
1540 : );
1541 4 : assert!(
1542 4 : norm("redirect", json!({"entities": [{"type": "Vehicle"}]})).is_ok(),
1543 : "redirect may register a whole type without attributes (4.3.6.3)"
1544 : );
1545 4 : assert!(
1546 4 : norm("sideways", full).is_err(),
1547 4 : "mode outside the 5.2.9 enum"
1548 4 : );
1549 4 : }
1550 :
1551 : /// 4.3.6.3: "Once an exclusive Context Source Registration has been
1552 : /// created, no further exclusive or redirect Context Source Registrations
1553 : /// can be created for that same combination of Entity ID and Attributes"
1554 : /// — while redirect × redirect overlap stays legal.
1555 : #[tokio::test]
1556 4 : async fn proxied_overlap_with_an_exclusive_registration_conflicts() {
1557 4 : let st = crate::state::AppState::new("me".into());
1558 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
1559 4 : let ctx = st.loader.core();
1560 28 : let mk = |id: &str, mode: &str, attr: &str| {
1561 28 : let doc = json!({
1562 28 : "id": id,
1563 28 : "type": "ContextSourceRegistration",
1564 28 : "endpoint": "http://peer:9090",
1565 28 : "mode": mode,
1566 28 : "information": [{
1567 28 : "entities": [{"id": "urn:ngsi-ld:Vehicle:v1", "type": "Vehicle"}],
1568 28 : "propertyNames": [attr]
1569 : }]
1570 : });
1571 28 : normalize_registration(doc.as_object().unwrap(), &ctx, false).expect("valid reg")
1572 28 : };
1573 4 : let seeded = mk(
1574 4 : "urn:ngsi-ld:ContextSourceRegistration:e1",
1575 4 : "exclusive",
1576 4 : "speed",
1577 4 : );
1578 4 : st.store
1579 4 : .create(
1580 4 : &tenant,
1581 4 : Kind::Registration,
1582 4 : "urn:ngsi-ld:ContextSourceRegistration:e1",
1583 4 : Value::Object(seeded),
1584 4 : )
1585 4 : .await
1586 4 : .expect("seed");
1587 4 : let overlap_exc = mk(
1588 4 : "urn:ngsi-ld:ContextSourceRegistration:e2",
1589 4 : "exclusive",
1590 4 : "speed",
1591 4 : );
1592 4 : assert!(
1593 4 : check_proxied_overlap(&st, &tenant, &overlap_exc, None, &ctx)
1594 4 : .await
1595 4 : .is_err(),
1596 : "second exclusive for the same (id, attr)"
1597 : );
1598 4 : let overlap_red = mk(
1599 4 : "urn:ngsi-ld:ContextSourceRegistration:r1",
1600 4 : "redirect",
1601 4 : "speed",
1602 4 : );
1603 4 : assert!(
1604 4 : check_proxied_overlap(&st, &tenant, &overlap_red, None, &ctx)
1605 4 : .await
1606 4 : .is_err(),
1607 : "redirect after an exclusive for the same combination"
1608 : );
1609 4 : let other_attr = mk(
1610 4 : "urn:ngsi-ld:ContextSourceRegistration:e3",
1611 4 : "exclusive",
1612 4 : "color",
1613 4 : );
1614 4 : assert!(
1615 4 : check_proxied_overlap(&st, &tenant, &other_attr, None, &ctx)
1616 4 : .await
1617 4 : .is_ok(),
1618 : "disjoint attribute is a different combination"
1619 : );
1620 : // the registration itself is not its own conflict (update path)
1621 4 : let self_doc = mk(
1622 4 : "urn:ngsi-ld:ContextSourceRegistration:e1",
1623 4 : "exclusive",
1624 4 : "speed",
1625 4 : );
1626 4 : assert!(check_proxied_overlap(
1627 4 : &st,
1628 4 : &tenant,
1629 4 : &self_doc,
1630 4 : Some("urn:ngsi-ld:ContextSourceRegistration:e1"),
1631 4 : &ctx
1632 : )
1633 4 : .await
1634 4 : .is_ok());
1635 : // redirect × redirect overlap is explicitly legal
1636 4 : let r2 = mk(
1637 4 : "urn:ngsi-ld:ContextSourceRegistration:r2",
1638 4 : "redirect",
1639 4 : "color",
1640 4 : );
1641 4 : st.store
1642 4 : .create(
1643 4 : &tenant,
1644 4 : Kind::Registration,
1645 4 : "urn:ngsi-ld:ContextSourceRegistration:r2",
1646 4 : Value::Object(r2),
1647 4 : )
1648 4 : .await
1649 4 : .expect("seed redirect");
1650 4 : let r3 = mk(
1651 4 : "urn:ngsi-ld:ContextSourceRegistration:r3",
1652 4 : "redirect",
1653 4 : "color",
1654 4 : );
1655 4 : assert!(check_proxied_overlap(&st, &tenant, &r3, None, &ctx)
1656 4 : .await
1657 4 : .is_ok());
1658 4 : }
1659 :
1660 : /// 5.2.8 Table 5.2.8-1 — an EntityInfo `type` is "String or String[]" —
1661 : /// applied to the 5.9.2.4 redirect rule: only an entity that matches the
1662 : /// registered Entity type conflicts, whichever spelling was registered.
1663 : #[tokio::test]
1664 4 : async fn clause_5_9_2_4_redirect_conflict_honours_the_array_form_entity_type() {
1665 4 : let st = crate::state::AppState::new("me".into());
1666 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
1667 4 : let ctx = st.loader.core();
1668 4 : let mut building = Map::new();
1669 4 : building.insert("id".into(), json!("urn:ngsi-ld:Building:b1"));
1670 4 : building.insert("type".into(), json!([ctx.expand_key("Building")]));
1671 4 : st.store
1672 4 : .create(
1673 4 : &tenant,
1674 4 : Kind::Entity,
1675 4 : "urn:ngsi-ld:Building:b1",
1676 4 : Value::Object(building),
1677 4 : )
1678 4 : .await
1679 4 : .expect("seed building");
1680 16 : let reg = |ty: Value| {
1681 16 : let doc = json!({
1682 16 : "id": "urn:ngsi-ld:ContextSourceRegistration:red1",
1683 16 : "type": "ContextSourceRegistration",
1684 16 : "endpoint": "http://peer:9090",
1685 16 : "mode": "redirect",
1686 16 : "information": [{"entities": [{"type": ty}]}]
1687 : });
1688 16 : normalize_registration(doc.as_object().expect("object"), &ctx, false).expect("valid")
1689 16 : };
1690 16 : async fn err(st: &AppState, tenant: &TenantId, doc: Map<String, Value>) -> Option<String> {
1691 16 : match check_entity_conflict(st, tenant, &doc).await {
1692 8 : Err(NgsiError::Conflict(m)) => Some(m),
1693 0 : Err(other) => panic!("unexpected error {other:?}"),
1694 8 : Ok(()) => None,
1695 : }
1696 16 : }
1697 4 : assert_eq!(
1698 4 : err(&st, &tenant, reg(json!(["Vehicle"]))).await,
1699 : None,
1700 : "a redirect for Vehicle must not conflict with a Building"
1701 : );
1702 4 : assert_eq!(
1703 4 : err(&st, &tenant, reg(json!("Vehicle"))).await,
1704 : None,
1705 : "same, in the string spelling"
1706 : );
1707 4 : let hit = err(&st, &tenant, reg(json!(["Building"])))
1708 4 : .await
1709 4 : .expect("the Building conflicts");
1710 4 : assert!(
1711 4 : hit.contains("urn:ngsi-ld:Building:b1"),
1712 : "the conflict names the existing entity: {hit}"
1713 : );
1714 4 : assert!(
1715 4 : err(&st, &tenant, reg(json!("Building"))).await.is_some(),
1716 4 : "same, string spelling"
1717 4 : );
1718 4 : }
1719 :
1720 : /// 5.9.2.4 redirect: "If an existing Entity already matches the
1721 : /// `Context Source Registration`, an error of type Conflict shall be
1722 : /// raised." An EntityInfo may identify its Entities by `idPattern`
1723 : /// alone (5.2.8) — a predicate no store can decide — and a
1724 : /// RegistrationInfo may carry several EntityInfo entries, so each
1725 : /// Entity read is asked about all of them.
1726 : #[tokio::test]
1727 4 : async fn clause_5_9_2_4_redirect_conflict_matches_an_id_pattern() {
1728 4 : let st = crate::state::AppState::new("me".into());
1729 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
1730 4 : let ctx = st.loader.core();
1731 8 : for (id, ty) in [
1732 4 : ("urn:ngsi-ld:Vehicle:v1", "Vehicle"),
1733 4 : ("urn:ngsi-ld:Device:d1", "Device"),
1734 4 : ] {
1735 8 : let mut e = Map::new();
1736 8 : e.insert("id".into(), json!(id));
1737 8 : e.insert("type".into(), json!([ctx.expand_key(ty)]));
1738 8 : st.store
1739 8 : .create(&tenant, Kind::Entity, id, Value::Object(e))
1740 8 : .await
1741 8 : .expect("seed");
1742 : }
1743 16 : let pat = |ents: Value| {
1744 16 : let doc = json!({
1745 16 : "id": "urn:ngsi-ld:ContextSourceRegistration:pat1",
1746 16 : "type": "ContextSourceRegistration",
1747 16 : "endpoint": "http://peer:9090",
1748 16 : "mode": "redirect",
1749 16 : "information": [{"entities": ents}]
1750 : });
1751 16 : normalize_registration(doc.as_object().expect("object"), &ctx, false).expect("valid")
1752 16 : };
1753 16 : async fn err(st: &AppState, tenant: &TenantId, doc: Map<String, Value>) -> Option<String> {
1754 16 : match check_entity_conflict(st, tenant, &doc).await {
1755 8 : Err(NgsiError::Conflict(m)) => Some(m),
1756 0 : Err(other) => panic!("unexpected error {other:?}"),
1757 8 : Ok(()) => None,
1758 : }
1759 16 : }
1760 4 : assert_eq!(
1761 4 : err(
1762 4 : &st,
1763 4 : &tenant,
1764 4 : pat(json!([{"idPattern": "^urn:ngsi-ld:Nothing:.*"}]))
1765 4 : )
1766 4 : .await,
1767 : None,
1768 : "a pattern no entity matches does not conflict"
1769 : );
1770 4 : let hit = err(
1771 4 : &st,
1772 4 : &tenant,
1773 4 : pat(json!([{"idPattern": "^urn:ngsi-ld:Vehicle:.*"}])),
1774 4 : )
1775 4 : .await
1776 4 : .expect("the Vehicle matches the pattern");
1777 4 : assert!(hit.contains("urn:ngsi-ld:Vehicle:v1"), "{hit}");
1778 4 : assert_eq!(
1779 4 : err(
1780 4 : &st,
1781 4 : &tenant,
1782 4 : pat(json!([{"idPattern": "^urn:ngsi-ld:Vehicle:.*", "type": "Device"}]))
1783 4 : )
1784 4 : .await,
1785 : None,
1786 : "the pattern and the type must both hold: the Vehicle is not a Device"
1787 : );
1788 : // The second EntityInfo is the one that matches: every selector of
1789 : // the RegistrationInfo is asked about every Entity, not only the first.
1790 4 : let hit = err(
1791 4 : &st,
1792 4 : &tenant,
1793 4 : pat(json!([
1794 4 : {"idPattern": "^urn:ngsi-ld:Nothing:.*"},
1795 4 : {"idPattern": "^urn:ngsi-ld:Device:.*"}
1796 4 : ])),
1797 4 : )
1798 4 : .await
1799 4 : .expect("the Device matches the second EntityInfo");
1800 4 : assert!(hit.contains("urn:ngsi-ld:Device:d1"), "{hit}");
1801 4 : }
1802 :
1803 : /// The Entities of the tenant are read a page at a time (a whole-tenant
1804 : /// read is refused above the store's row ceiling, 5.5.6, and this check
1805 : /// has no TooManyResults to raise), so a conflict that sits beyond the
1806 : /// first page must still be found.
1807 : #[tokio::test]
1808 4 : async fn clause_5_9_2_4_a_conflict_past_the_first_page_is_found() {
1809 4 : let st = crate::state::AppState::new("me".into());
1810 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
1811 4 : let ctx = st.loader.core();
1812 4 : let device = ctx.expand_key("Device");
1813 4000 : for i in 0..SCAN_PAGE {
1814 4000 : let id = format!("urn:ngsi-ld:Device:{i:06}");
1815 4000 : let mut e = Map::new();
1816 4000 : e.insert("id".into(), json!(id));
1817 4000 : e.insert("type".into(), json!([device]));
1818 4000 : st.store
1819 4000 : .create(&tenant, Kind::Entity, &id, Value::Object(e))
1820 4000 : .await
1821 4000 : .expect("seed");
1822 : }
1823 : // sorts after every Device above, so it is only reached on page two
1824 4 : let mut zebra = Map::new();
1825 4 : zebra.insert("id".into(), json!("urn:ngsi-ld:Zebra:z1"));
1826 4 : zebra.insert("type".into(), json!([ctx.expand_key("Zebra")]));
1827 4 : st.store
1828 4 : .create(
1829 4 : &tenant,
1830 4 : Kind::Entity,
1831 4 : "urn:ngsi-ld:Zebra:z1",
1832 4 : Value::Object(zebra),
1833 4 : )
1834 4 : .await
1835 4 : .expect("seed");
1836 4 : let doc = json!({
1837 4 : "id": "urn:ngsi-ld:ContextSourceRegistration:pat2",
1838 4 : "type": "ContextSourceRegistration",
1839 4 : "endpoint": "http://peer:9090",
1840 4 : "mode": "redirect",
1841 4 : "information": [{"entities": [{"idPattern": "^urn:ngsi-ld:Zebra:.*"}]}]
1842 : });
1843 4 : let norm =
1844 4 : normalize_registration(doc.as_object().expect("object"), &ctx, false).expect("valid");
1845 4 : match check_entity_conflict(&st, &tenant, &norm).await {
1846 4 : Err(NgsiError::Conflict(m)) => assert!(m.contains("urn:ngsi-ld:Zebra:z1"), "{m}"),
1847 4 : other => panic!("the conflict on page two was missed: {other:?}"),
1848 4 : }
1849 4 : }
1850 :
1851 : /// 5.9.2.4: an exclusive registration conflicts with an existing Entity
1852 : /// only when "the existing Entity contains any of the Attributes defined
1853 : /// in the registration".
1854 : #[tokio::test]
1855 4 : async fn clause_5_9_2_4_exclusive_conflict_needs_a_registered_attribute() {
1856 4 : let st = crate::state::AppState::new("me".into());
1857 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
1858 4 : let ctx = st.loader.core();
1859 4 : let mut vehicle = Map::new();
1860 4 : vehicle.insert("id".into(), json!("urn:ngsi-ld:Vehicle:v1"));
1861 4 : vehicle.insert("type".into(), json!([ctx.expand_key("Vehicle")]));
1862 4 : vehicle.insert(ctx.expand_key("color"), json!([{"type": "Property"}]));
1863 4 : st.store
1864 4 : .create(
1865 4 : &tenant,
1866 4 : Kind::Entity,
1867 4 : "urn:ngsi-ld:Vehicle:v1",
1868 4 : Value::Object(vehicle),
1869 4 : )
1870 4 : .await
1871 4 : .expect("seed vehicle");
1872 12 : let reg = |attr: &str, id: &str| {
1873 12 : let doc = json!({
1874 12 : "id": "urn:ngsi-ld:ContextSourceRegistration:exc1",
1875 12 : "type": "ContextSourceRegistration",
1876 12 : "endpoint": "http://peer:9090",
1877 12 : "mode": "exclusive",
1878 12 : "information": [{
1879 12 : "entities": [{"id": id, "type": "Vehicle"}],
1880 12 : "propertyNames": [attr]
1881 : }]
1882 : });
1883 12 : normalize_registration(doc.as_object().expect("object"), &ctx, false).expect("valid")
1884 12 : };
1885 4 : assert!(
1886 4 : check_entity_conflict(&st, &tenant, ®("speed", "urn:ngsi-ld:Vehicle:v1"))
1887 4 : .await
1888 4 : .is_ok(),
1889 : "the entity carries no speed Attribute"
1890 : );
1891 4 : assert!(
1892 4 : check_entity_conflict(&st, &tenant, ®("color", "urn:ngsi-ld:Vehicle:v2"))
1893 4 : .await
1894 4 : .is_ok(),
1895 : "another entity id is not this entity"
1896 : );
1897 4 : assert!(
1898 4 : check_entity_conflict(&st, &tenant, ®("color", "urn:ngsi-ld:Vehicle:v1"))
1899 4 : .await
1900 4 : .is_err(),
1901 4 : "the entity carries the registered color Attribute"
1902 4 : );
1903 4 : }
1904 :
1905 : /// 6.8.3.2 Table 6.8.3.2-1: the `type` parameter of Query Context Source
1906 : /// Registrations is "Selection of Entity Types as per clause 4.17" — a
1907 : /// selection expression, not a comma-separated list of terms.
1908 : #[tokio::test]
1909 4 : async fn clause_4_17_type_selection_queries_registrations() {
1910 4 : let st = crate::state::AppState::new("me".into());
1911 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
1912 4 : let ctx = st.loader.core();
1913 8 : let reg = |id: &str, ty: Value| {
1914 8 : let doc = json!({
1915 8 : "id": id,
1916 8 : "type": "ContextSourceRegistration",
1917 8 : "endpoint": "http://peer:9090",
1918 8 : "information": [{"entities": [{"type": ty}]}]
1919 : });
1920 8 : normalize_registration(doc.as_object().expect("object"), &ctx, false).expect("ok")
1921 8 : };
1922 8 : async fn seed(st: &AppState, tenant: &TenantId, id: &str, doc: Map<String, Value>) {
1923 8 : st.store
1924 8 : .create(tenant, Kind::Registration, id, Value::Object(doc))
1925 8 : .await
1926 8 : .expect("seed");
1927 8 : }
1928 4 : let both = "urn:ngsi-ld:ContextSourceRegistration:both";
1929 4 : let home = "urn:ngsi-ld:ContextSourceRegistration:home";
1930 4 : seed(&st, &tenant, both, reg(both, json!(["Home", "Vehicle"]))).await;
1931 4 : seed(&st, &tenant, home, reg(home, json!("Home"))).await;
1932 12 : let ids = |sel: &str| {
1933 12 : let st = st.clone();
1934 12 : let sel = sel.to_owned();
1935 12 : async move {
1936 12 : let params = HashMap::from([("type".to_owned(), sel)]);
1937 12 : let resp =
1938 12 : query_registrations(State(st), CleanParams(params), HeaderMap::new()).await;
1939 12 : assert_eq!(resp.status(), StatusCode::OK);
1940 12 : let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
1941 12 : .await
1942 12 : .expect("body");
1943 12 : let body: Value = serde_json::from_slice(&bytes).expect("json list");
1944 12 : body.as_array()
1945 12 : .expect("array")
1946 12 : .iter()
1947 12 : .filter_map(|r| r.get("id").and_then(Value::as_str).map(str::to_owned))
1948 12 : .collect::<Vec<String>>()
1949 12 : }
1950 12 : };
1951 4 : let conj = ids("(Home;Vehicle)").await;
1952 4 : assert!(
1953 4 : conj.contains(&"urn:ngsi-ld:ContextSourceRegistration:both".to_owned()),
1954 : "a registration declaring both types matches the conjunction: {conj:?}"
1955 : );
1956 4 : assert!(
1957 4 : !conj.contains(&"urn:ngsi-ld:ContextSourceRegistration:home".to_owned()),
1958 : "a registration declaring only Home must not match: {conj:?}"
1959 : );
1960 4 : let alt = ids("Vehicle,Home").await;
1961 4 : assert_eq!(alt.len(), 2, "a comma list is a disjunction: {alt:?}");
1962 4 : let none = ids("Parking").await;
1963 4 : assert!(
1964 4 : none.is_empty(),
1965 4 : "no registration declares Parking: {none:?}"
1966 4 : );
1967 4 : }
1968 :
1969 : /// 5.9.2.4: "If expiresAt is a date and time in the past, an error of
1970 : /// type BadRequestData shall be raised" — the comparison is over the
1971 : /// instant, not over the spelling, so an expiresAt written with fewer
1972 : /// sub-second digits than the server's own timestamp is still past.
1973 : #[test]
1974 4 : fn clause_5_9_2_4_past_expires_at_whatever_the_fraction_spelling() {
1975 4 : let ctx = Loader::new().core();
1976 4 : for _ in 0..8 {
1977 32 : let now = now_iso();
1978 32 : if now.len() < 24 || &now[20..23] == "000" {
1979 0 : continue; // no sub-second gap to compare against
1980 32 : }
1981 32 : let past = format!("{}Z", &now[..19]); // same second, no fraction
1982 32 : let doc = json!({
1983 32 : "id": "urn:ngsi-ld:ContextSourceRegistration:exp1",
1984 32 : "type": "ContextSourceRegistration",
1985 32 : "endpoint": "http://peer:9090",
1986 32 : "expiresAt": past,
1987 32 : "information": [{"entities": [{"type": "Vehicle"}]}]
1988 : });
1989 32 : assert!(
1990 32 : normalize_registration(doc.as_object().expect("object"), &ctx, false).is_err(),
1991 : "expiresAt {past} is in the past of {now}"
1992 : );
1993 : }
1994 4 : }
1995 :
1996 : /// The registration body's cardinality caps are the only bound on the
1997 : /// index rows one create materialises: the last accepted size and the
1998 : /// first rejected one both have to hold.
1999 : #[test]
2000 4 : fn registration_cardinality_caps_hold_at_the_edge() {
2001 4 : let ctx = Loader::new().core();
2002 1036 : let info = |n: usize| {
2003 1036 : json!({"entities": [{"type": "Vehicle"}], "propertyNames":
2004 2056 : (0..n).map(|i| Value::String(format!("p{i}"))).collect::<Vec<_>>()})
2005 1036 : };
2006 32 : let mk = |information: Value| {
2007 32 : let doc = json!({
2008 32 : "id": "urn:ngsi-ld:ContextSourceRegistration:cap1",
2009 32 : "type": "ContextSourceRegistration",
2010 32 : "endpoint": "http://peer:9090",
2011 32 : "information": information
2012 : });
2013 32 : normalize_registration(doc.as_object().expect("object"), &ctx, false)
2014 32 : };
2015 1028 : let many = |n: usize| Value::Array((0..n).map(|_| info(1)).collect());
2016 4 : assert!(mk(many(MAX_INFORMATION)).is_ok(), "the cap itself is legal");
2017 4 : let over = mk(many(MAX_INFORMATION + 1)).expect_err("over the cap");
2018 4 : assert!(matches!(over, NgsiError::BadRequestData(_)), "{over:?}");
2019 : // the status is what the client sees, and 403 would tell it to
2020 : // narrow a query it never sent
2021 4 : assert_eq!(over.status(), 400);
2022 4 : assert!(mk(json!([info(MAX_INFO_MEMBERS)])).is_ok());
2023 4 : assert!(matches!(
2024 4 : mk(json!([info(MAX_INFO_MEMBERS + 1)])),
2025 : Err(NgsiError::BadRequestData(_))
2026 : ));
2027 8 : let entities = |n: usize| {
2028 8 : json!([{"entities": (0..n)
2029 1028 : .map(|i| json!({"id": format!("urn:ngsi-ld:Vehicle:v{i}"), "type": "Vehicle"}))
2030 8 : .collect::<Vec<_>>()}])
2031 8 : };
2032 4 : assert!(mk(entities(MAX_INFO_MEMBERS)).is_ok());
2033 4 : assert!(matches!(
2034 4 : mk(entities(MAX_INFO_MEMBERS + 1)),
2035 : Err(NgsiError::BadRequestData(_))
2036 : ));
2037 8 : let rels = |n: usize| {
2038 8 : json!([{"entities": [{"type": "Vehicle"}], "relationshipNames":
2039 1028 : (0..n).map(|i| Value::String(format!("r{i}"))).collect::<Vec<_>>()}])
2040 8 : };
2041 4 : assert!(mk(rels(MAX_INFO_MEMBERS)).is_ok());
2042 4 : assert!(matches!(
2043 4 : mk(rels(MAX_INFO_MEMBERS + 1)),
2044 : Err(NgsiError::BadRequestData(_))
2045 : ));
2046 4 : }
2047 :
2048 : /// 4.3.6.6: the four processed contextSourceInfo keys have
2049 : /// constrained value spaces, checked at registration time.
2050 : #[test]
2051 4 : fn context_source_info_reserved_keys_are_validated() {
2052 4 : let ctx = Loader::new().core();
2053 36 : let mk = |key: &str, value: &str| {
2054 36 : json!({
2055 36 : "id": "urn:ngsi-ld:ContextSourceRegistration:csi1",
2056 36 : "type": "ContextSourceRegistration",
2057 36 : "endpoint": "http://peer:9090",
2058 36 : "information": [{"entities": [{"type": "Building"}]}],
2059 36 : "contextSourceInfo": [{"key": key, "value": value}]
2060 : })
2061 36 : };
2062 36 : let ok = |key: &str, value: &str| {
2063 36 : normalize_registration(mk(key, value).as_object().unwrap(), &ctx, false).is_ok()
2064 36 : };
2065 4 : assert!(ok("accept", "application/json"));
2066 4 : assert!(ok("contentType", "application/ld+json"));
2067 4 : assert!(!ok("accept", "text/html"), "MIME outside 4.3.6.6's list");
2068 4 : assert!(!ok("contentType", "application/geo+json"));
2069 4 : assert!(ok("jsonldContext", "https://example.org/ctx.jsonld"));
2070 4 : assert!(!ok("jsonldContext", "not a url"));
2071 4 : assert!(ok("ngsildConformance", "1.6"));
2072 4 : assert!(!ok("ngsildConformance", "latest"));
2073 : // ordinary custom keys stay free-form
2074 4 : assert!(ok("Authorization", "Bearer abc"));
2075 4 : }
2076 :
2077 : /// Table 5.2.9-1 refreshRate and Table 5.2.34-1 cacheDuration are ISO 8601
2078 : /// durations. The grammar is `P[nY][nM][nW][nD][T[nH][nM][nS]]`: every
2079 : /// component is a number, the components keep their order, and the time
2080 : /// units live after the `T` — a value that is none of those is not a
2081 : /// duration, whatever it looks like.
2082 : #[test]
2083 4 : fn table_5_2_9_1_refresh_rate_takes_an_iso_8601_duration() {
2084 48 : for good in [
2085 4 : "P1Y",
2086 4 : "P1M",
2087 4 : "P1W",
2088 4 : "P1D",
2089 4 : "PT1H",
2090 4 : "PT1M",
2091 4 : "PT30S",
2092 4 : "P1Y2M3DT4H5M6S",
2093 4 : "PT0.5S",
2094 4 : "PT0,5S",
2095 4 : "P1Y2M",
2096 4 : "P1DT12H",
2097 4 : ] {
2098 48 : assert!(valid_iso8601_duration(good), "{good} is a duration");
2099 : }
2100 76 : for bad in [
2101 4 : "", "P", "PT", "1Y", "P1", "P1X", "p1d", // the designators are upper case
2102 4 : "P1H", // an hour is a time component and belongs after the T
2103 4 : "PT1D", // a day is a date component and belongs before it
2104 4 : "P1D2M", // out of order: months precede days
2105 4 : "P,D", "P.D", "P..D", // separators are not a number
2106 4 : "P-1D", "P+1D", "P 1D", "PT1S1S", "P1DT", "1PD",
2107 4 : ] {
2108 76 : assert!(!valid_iso8601_duration(bad), "{bad:?} is not a duration");
2109 : }
2110 4 : }
2111 :
2112 : /// 5.5.6 + 5.10.2.4: the registration query filters before it pages, so
2113 : /// every match is held at once. A tenant at the 100 000-registration
2114 : /// target answers one `type=` selector with every one of them, which is
2115 : /// the "so many results that can potentially exhaust … server resources"
2116 : /// 5.5.6 names — the query is refused at the ceiling instead of building
2117 : /// the answer.
2118 : #[tokio::test]
2119 4 : async fn clause_5_5_6_a_registration_query_stops_at_the_fold_ceiling() {
2120 4 : let st = AppState::new("antares-csr-ceiling".into());
2121 4 : let tenant = TenantId::default();
2122 20 : for i in 0..5 {
2123 20 : let id = format!("urn:ngsi-ld:ContextSourceRegistration:ceil{i}");
2124 20 : st.store
2125 20 : .create(
2126 20 : &tenant,
2127 20 : Kind::Registration,
2128 20 : &id,
2129 20 : json!({
2130 20 : "id": id,
2131 20 : "type": "ContextSourceRegistration",
2132 20 : "endpoint": "http://peer:9090",
2133 20 : "information": [{"entities": [{"type": "Building"}]}],
2134 20 : }),
2135 20 : )
2136 20 : .await
2137 20 : .expect("store the registration");
2138 : }
2139 4 : let all = collect_matching(&st, &tenant, |_| true, 100)
2140 4 : .await
2141 4 : .expect("under the ceiling");
2142 4 : assert_eq!(all.len(), 5, "every registration matches");
2143 4 : let err = collect_matching(&st, &tenant, |_| true, 2)
2144 4 : .await
2145 4 : .expect_err("a match set over the ceiling is refused");
2146 4 : assert!(
2147 4 : matches!(err, NgsiError::TooManyResults(_)),
2148 : "5.5.6 names TooManyResults, got {err:?}"
2149 : );
2150 : // the ceiling counts MATCHES, not documents walked: a narrow query
2151 : // over the same tenant still answers
2152 4 : let narrow = collect_matching(
2153 4 : &st,
2154 4 : &tenant,
2155 20 : |d| d["id"] == json!("urn:ngsi-ld:ContextSourceRegistration:ceil3"),
2156 : 2,
2157 : )
2158 4 : .await
2159 4 : .expect("one match is under any ceiling");
2160 4 : assert_eq!(narrow.len(), 1);
2161 4 : }
2162 :
2163 : /// 6.3.19: "Key and value members shall adhere to IETF RFC 7230 …
2164 : /// definitions concerning HTTP headers". A pair that is not a header is
2165 : /// refused where 5.9.2.4 refuses registration content, not carried until
2166 : /// the first forward — where the request cannot be built at all, and the
2167 : /// registration that caused it is nowhere in the failure.
2168 : #[test]
2169 4 : fn clause_6_3_19_a_pair_that_is_not_a_header_is_refused() {
2170 4 : let ctx = Loader::new().core();
2171 52 : let mk = |key: &str, value: &str| {
2172 52 : json!({
2173 52 : "id": "urn:ngsi-ld:ContextSourceRegistration:csi2",
2174 52 : "type": "ContextSourceRegistration",
2175 52 : "endpoint": "http://peer:9090",
2176 52 : "information": [{"entities": [{"type": "Building"}]}],
2177 52 : "contextSourceInfo": [{"key": key, "value": value}]
2178 : })
2179 52 : };
2180 52 : let ok = |key: &str, value: &str| {
2181 52 : normalize_registration(mk(key, value).as_object().expect("object"), &ctx, false).is_ok()
2182 52 : };
2183 : // RFC 7230 field-name is a token: no separators, no space, no CTL
2184 24 : for key in [
2185 4 : "X-Injected\r\nX-Second",
2186 4 : "X Injected",
2187 4 : "X:Injected",
2188 4 : "",
2189 4 : "X-Inj\u{0000}ected",
2190 4 : "Über-Header",
2191 4 : ] {
2192 24 : assert!(!ok(key, "value"), "{key:?} is not an RFC 7230 field-name");
2193 : }
2194 : // RFC 7230 field-value carries no CR, LF or NUL
2195 16 : for value in ["a\r\nX-Injected: 1", "a\nb", "a\rb", "a\u{0000}b"] {
2196 16 : assert!(
2197 16 : !ok("X-Custom", value),
2198 : "{value:?} is not an RFC 7230 field-value"
2199 : );
2200 : }
2201 : // and the shapes a real registration uses stay accepted
2202 4 : assert!(ok("X-Custom", "urn:ngsi-ld:request"));
2203 4 : assert!(ok("X-Api-Key", "a b c"));
2204 4 : assert!(ok("Authorization", "Bearer abc.def-ghi_jkl"));
2205 4 : }
2206 : }
2207 :
2208 : #[cfg(test)]
2209 : mod concurrent_create_5_9_2_4 {
2210 : use super::*;
2211 : use std::collections::HashMap;
2212 :
2213 : /// 5.9.2.4 decides a conflict against the registrations "already"
2214 : /// present in one tenant, and two tenants' sets are disjoint: a write
2215 : /// in one tenant never waits for the check-then-write section of
2216 : /// another, while a second write in the same tenant does.
2217 : #[tokio::test]
2218 4 : async fn the_write_section_of_one_tenant_does_not_hold_another() {
2219 : use std::time::Duration;
2220 : use tokio::time::timeout;
2221 4 : let a = TenantId::new("locka").expect("tenant");
2222 4 : let b = TenantId::new("lockb").expect("tenant");
2223 4 : let held = registration_write_lock(&a).await;
2224 4 : let other = timeout(Duration::from_millis(200), registration_write_lock(&b)).await;
2225 4 : assert!(
2226 4 : other.is_ok(),
2227 : "tenant b waited behind tenant a's registration write"
2228 : );
2229 4 : let same = timeout(Duration::from_millis(200), registration_write_lock(&a)).await;
2230 4 : assert!(same.is_err(), "a second write in the same tenant must wait");
2231 4 : drop(held);
2232 4 : let again = timeout(Duration::from_millis(200), registration_write_lock(&a)).await;
2233 4 : assert!(again.is_ok(), "the section is free once its holder is done");
2234 4 : }
2235 :
2236 1200 : fn exclusive_body(n: usize) -> Bytes {
2237 1200 : Bytes::from(format!(
2238 : r#"{{"id":"urn:ngsi-ld:ContextSourceRegistration:race{n}",
2239 : "type":"ContextSourceRegistration",
2240 : "endpoint":"http://peer:9090",
2241 : "mode":"exclusive",
2242 : "information":[{{"entities":[{{"id":"urn:ngsi-ld:Vehicle:race",
2243 : "type":"Vehicle"}}],
2244 : "propertyNames":["speed"]}}]}}"#
2245 : ))
2246 1200 : }
2247 :
2248 1600 : fn json_headers() -> HeaderMap {
2249 1600 : let mut h = HeaderMap::new();
2250 1600 : h.insert(
2251 1600 : axum::http::header::CONTENT_TYPE,
2252 1600 : "application/json".parse().expect("header value"),
2253 : );
2254 1600 : h
2255 1600 : }
2256 :
2257 : /// 5.9.2.4: "If an exclusive or redirect Context Source Registration
2258 : /// already matches against the Entity ID (URI) and any of the Attributes
2259 : /// defined in the registration, an error of type Conflict shall be
2260 : /// raised." Requests racing each other must not both slip past that
2261 : /// check: whatever the interleaving, one registration is stored and
2262 : /// every other request gets the Conflict.
2263 : #[tokio::test(flavor = "multi_thread", worker_threads = 8)]
2264 4 : async fn concurrent_exclusive_creates_store_exactly_one_registration() {
2265 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
2266 100 : for round in 0..25 {
2267 100 : let st = crate::state::AppState::new("me".into());
2268 100 : let gate = std::sync::Arc::new(tokio::sync::Barrier::new(8));
2269 100 : let mut tasks = Vec::new();
2270 800 : for n in 0..8 {
2271 800 : let (st, gate) = (st.clone(), gate.clone());
2272 800 : tasks.push(tokio::spawn(async move {
2273 800 : gate.wait().await;
2274 800 : create_registration(
2275 800 : State(st),
2276 800 : CleanParams(HashMap::new()),
2277 800 : json_headers(),
2278 800 : exclusive_body(n),
2279 4 : )
2280 800 : .await
2281 800 : .status()
2282 800 : }));
2283 4 : }
2284 100 : let mut created = 0;
2285 800 : for t in tasks {
2286 800 : match t.await.expect("task") {
2287 800 : StatusCode::CREATED => created += 1,
2288 700 : StatusCode::CONFLICT => {}
2289 4 : other => panic!("round {round}: unexpected status {other}"),
2290 4 : }
2291 4 : }
2292 100 : let stored = st
2293 100 : .store
2294 100 : .list(&tenant, Kind::Registration)
2295 100 : .await
2296 100 : .expect("registrations");
2297 100 : assert_eq!(
2298 100 : stored.len(),
2299 4 : 1,
2300 4 : "round {round}: 5.9.2.4 forbids a second exclusive registration \
2301 4 : for the same Entity ID and Attribute"
2302 4 : );
2303 100 : assert_eq!(created, 1, "round {round}: exactly one create may succeed");
2304 4 : }
2305 4 : }
2306 :
2307 : /// 5.9.3.4 applies the same 5.9.2.4 mode rules to the merged document,
2308 : /// so two patches that each flip an inclusive registration to exclusive
2309 : /// over one Entity ID and Attribute may not both take effect.
2310 : #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2311 4 : async fn concurrent_patches_to_exclusive_leave_one_exclusive_registration() {
2312 4 : let tenant = antares_model::TenantId::new("default").expect("tenant");
2313 100 : for round in 0..25 {
2314 100 : let st = crate::state::AppState::new("me".into());
2315 400 : for n in 0..4 {
2316 400 : let body = Bytes::from(
2317 400 : String::from_utf8_lossy(&exclusive_body(n))
2318 400 : .replace(r#""mode":"exclusive","#, ""),
2319 4 : );
2320 400 : let status = create_registration(
2321 400 : State(st.clone()),
2322 400 : CleanParams(HashMap::new()),
2323 400 : json_headers(),
2324 400 : body,
2325 4 : )
2326 400 : .await
2327 400 : .status();
2328 400 : assert_eq!(status, StatusCode::CREATED, "round {round}: seed {n}");
2329 4 : }
2330 100 : let gate = std::sync::Arc::new(tokio::sync::Barrier::new(4));
2331 100 : let mut tasks = Vec::new();
2332 400 : for n in 0..4 {
2333 400 : let (st, gate) = (st.clone(), gate.clone());
2334 400 : tasks.push(tokio::spawn(async move {
2335 400 : gate.wait().await;
2336 400 : update_registration(
2337 400 : State(st),
2338 400 : Path(format!("urn:ngsi-ld:ContextSourceRegistration:race{n}")),
2339 400 : CleanParams(HashMap::new()),
2340 400 : json_headers(),
2341 400 : Bytes::from(r#"{"mode":"exclusive"}"#),
2342 4 : )
2343 400 : .await
2344 400 : .status()
2345 400 : }));
2346 4 : }
2347 100 : let mut patched = 0;
2348 400 : for t in tasks {
2349 400 : match t.await.expect("task") {
2350 400 : StatusCode::NO_CONTENT => patched += 1,
2351 300 : StatusCode::CONFLICT => {}
2352 4 : other => panic!("round {round}: unexpected status {other}"),
2353 4 : }
2354 4 : }
2355 100 : let exclusive = st
2356 100 : .store
2357 100 : .list(&tenant, Kind::Registration)
2358 100 : .await
2359 100 : .expect("registrations")
2360 100 : .iter()
2361 400 : .filter(|r| r.get("mode").and_then(Value::as_str) == Some("exclusive"))
2362 100 : .count();
2363 100 : assert_eq!(
2364 4 : exclusive, 1,
2365 4 : "round {round}: 5.9.2.4 forbids a second exclusive registration \
2366 4 : for the same Entity ID and Attribute"
2367 4 : );
2368 100 : assert_eq!(patched, 1, "round {round}: exactly one patch may succeed");
2369 4 : }
2370 4 : }
2371 : }
|