Line data Source code
1 : // SPDX-License-Identifier: EUPL-1.2
2 : //! Extra HTTP surfaces, registered rather than hard-wired.
3 : //!
4 : //! CIM 009 6.2 owns `/ngsi-ld/v1`; everything a deployment serves beside it
5 : //! lives under a reserved prefix, so an added surface can never shadow,
6 : //! extend or contradict a spec resource. `/q` is this broker's operational
7 : //! ground and belongs to the admin surface; `/x` and anything under it is
8 : //! free for a deployment. `/ex` is neither: it is the broker's own
9 : //! peer-facing wire (the 5.8.1.4 notification receiver, ADR-0019), a core
10 : //! route no surface can claim, since a prefix outside `/q` and `/x` is
11 : //! refused below.
12 :
13 : use crate::AppState;
14 : use axum::Router;
15 : use serde_json::Value;
16 :
17 : /// One mountable group of routes outside the NGSI-LD API root.
18 : pub trait ApiSurface: Send + Sync {
19 : /// The name `ANTARES_API_SURFACES` selects it by, and the key it
20 : /// appears under in `/q/health`.
21 : fn name(&self) -> &str;
22 :
23 : /// Where the routes mount: `/q`, or `/x` and anything below it. The
24 : /// routes the surface returns are relative to this.
25 : fn prefix(&self) -> &str;
26 :
27 : /// The routes, relative to `prefix`. The broker supplies the state.
28 : fn router(&self, st: AppState) -> Router<AppState>;
29 :
30 : /// What `/q/health` reports about it, beside its prefix.
31 : fn version_info(&self) -> Value;
32 : }
33 :
34 : /// The prefixes a surface may mount under. `/q` is the broker's operational
35 : /// ground; `/x` is the deployment's.
36 : const RESERVED: [&str; 2] = ["/q", "/x"];
37 :
38 : /// A prefix a surface is allowed to claim: exactly one of the reserved
39 : /// roots, or a path below `/x`. Everything else — the NGSI-LD API root
40 : /// above all — is refused, since a surface that could mount there would
41 : /// make conformance a function of deployment configuration.
42 168 : pub(crate) fn check_prefix(prefix: &str) -> Result<(), String> {
43 168 : if RESERVED.contains(&prefix) || prefix.starts_with("/x/") {
44 88 : return Ok(());
45 80 : }
46 80 : Err(format!(
47 80 : "api surface prefix {prefix:?} is not reserved; a surface mounts at /q, at /x, \
48 80 : or below /x — never under the NGSI-LD API root"
49 80 : ))
50 168 : }
51 :
52 : /// Two prefixes claim the same ground when they are equal or one nests
53 : /// inside the other. Merging both would leave the winner to route-matching
54 : /// order, so it is refused where it can still be a startup error.
55 84 : pub(crate) fn overlaps(a: &str, b: &str) -> bool {
56 84 : a == b || a.starts_with(&format!("{b}/")) || b.starts_with(&format!("{a}/"))
57 84 : }
58 :
59 : #[cfg(test)]
60 : mod tests {
61 : use super::*;
62 :
63 : #[test]
64 4 : fn only_the_reserved_prefixes_are_mountable() {
65 16 : for ok in ["/q", "/x", "/x/plugin", "/x/a/b"] {
66 16 : assert!(check_prefix(ok).is_ok(), "{ok}");
67 : }
68 56 : for bad in [
69 4 : "/ngsi-ld",
70 4 : "/ngsi-ld/v1",
71 4 : "/ngsi-ld/v1/entities",
72 4 : "/",
73 4 : "",
74 4 : "/entities",
75 4 : "/qq",
76 4 : "/q/health",
77 4 : "x",
78 4 : "q",
79 4 : "//x",
80 4 : // the peer-facing wire of 5.8.1.4 is a core route, so a surface
81 4 : // cannot claim it or anything under it
82 4 : "/ex",
83 4 : "/ex/v1",
84 4 : "/ex/v1/remote-notify",
85 4 : ] {
86 56 : let err = check_prefix(bad).expect_err(bad);
87 56 : assert!(err.contains(bad), "the message names the prefix: {err}");
88 : }
89 4 : }
90 :
91 : #[test]
92 4 : fn a_prefix_collides_with_itself_and_with_its_ancestors() {
93 4 : assert!(overlaps("/x", "/x"));
94 4 : assert!(overlaps("/x", "/x/deeper"));
95 4 : assert!(overlaps("/x/deeper", "/x"));
96 4 : assert!(!overlaps("/q", "/x"));
97 4 : assert!(!overlaps("/x/a", "/x/b"));
98 : // a shared text prefix is not a shared path segment
99 4 : assert!(!overlaps("/x/ab", "/x/abc"));
100 4 : }
101 : }
|