Skip to main content

openbao/
compatibility.rs

1//! OpenBao server-version compatibility value types.
2//!
3//! These types model stable OpenBao releases. They deliberately reject
4//! prerelease and build metadata so a compatibility profile always selects an
5//! exact reviewed server release.
6//!
7//! This module also exposes a generated read-only registry of secret-free route
8//! templates across 22 locked OpenBao releases from 2.0.0 through 2.6.0.
9//! Registry evidence reports what exact tagged documentation contains. Client
10//! compatibility policies can verify and cache the stable
11//! version returned by `/sys/health`, or explicitly select an assumed promoted
12//! profile where probing is unavailable. The internal typed dispatcher binds
13//! logical SDK endpoints to reviewed operation variants; endpoint families
14//! adopt that dispatcher in the ordered migration commits.
15
16use core::{fmt, str::FromStr};
17
18use crate::{Error, Result};
19
20const MAX_VERSION_BYTES: usize = 64;
21const MAX_REQUIREMENT_BYTES: usize = (MAX_VERSION_BYTES * 2) + 3;
22
23/// High-level compatibility policy selected for one client instance.
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
25pub struct OpenBaoCompatibilityPolicy {
26    value: OpenBaoCompatibilityPolicyValue,
27}
28
29/// Public classification of an OpenBao compatibility policy.
30#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
31#[non_exhaustive]
32pub enum OpenBaoCompatibilityPolicyKind {
33    /// Detect the server and require one exact runtime-approved release.
34    Exact,
35    /// Detect the server and require a release inside one approved closed range.
36    Range,
37    /// Detect the server and require any exact runtime-approved release.
38    AutomaticStrict,
39    /// Select one exact runtime-approved profile without querying the server.
40    Assumed,
41    /// Detect the server and explicitly tolerate a version newer than the registry.
42    AcknowledgedUnknownNewer,
43}
44
45#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
46enum OpenBaoCompatibilityPolicyValue {
47    Exact(OpenBaoVersion),
48    Range(OpenBaoVersionRequirement),
49    AutomaticStrict,
50    Assumed(OpenBaoVersion),
51    AcknowledgedUnknownNewer,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub(crate) enum OpenBaoCompatibilityFailure {
56    VersionMismatch {
57        detected: OpenBaoVersion,
58        requirement: OpenBaoVersionRequirement,
59    },
60    UnknownVersion(OpenBaoVersion),
61}
62
63/// Explicit acknowledgement required to tolerate an unknown newer server.
64///
65/// This policy uses the newest reviewed compatibility profile when the server
66/// reports a later stable version. It cannot prove that removed or changed
67/// operations remain compatible. Prefer strict mode and add a reviewed release
68/// profile instead.
69#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
70pub struct UnknownNewerOpenBaoAcknowledgement(());
71
72impl UnknownNewerOpenBaoAcknowledgement {
73    /// Acknowledges the compatibility risk of using the newest known profile.
74    #[must_use]
75    pub const fn acknowledge() -> Self {
76        Self(())
77    }
78}
79
80/// Verification state reported for one client instance.
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82#[non_exhaustive]
83pub enum OpenBaoCompatibilityStatus {
84    /// No compatibility policy was selected and no server version was checked.
85    Unverified,
86    /// The server version was detected and matched a runtime-approved profile.
87    Verified,
88    /// A runtime-approved profile was explicitly selected without a server probe.
89    Assumed,
90    /// A newer server was detected and the caller acknowledged using the latest profile.
91    AcknowledgedUnknownNewer,
92}
93
94/// Secret-free compatibility result for one client instance.
95#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
96pub struct OpenBaoCompatibilityReport {
97    status: OpenBaoCompatibilityStatus,
98    policy: Option<OpenBaoCompatibilityPolicyKind>,
99    requirement: Option<OpenBaoVersionRequirement>,
100    detected_version: Option<OpenBaoVersion>,
101    profile_version: Option<OpenBaoVersion>,
102}
103
104impl OpenBaoCompatibilityReport {
105    /// Verification state for this report.
106    pub const fn status(self) -> OpenBaoCompatibilityStatus {
107        self.status
108    }
109
110    /// Selected policy classification, or `None` for an unverified client.
111    pub const fn policy(self) -> Option<OpenBaoCompatibilityPolicyKind> {
112        self.policy
113    }
114
115    /// Exact or closed version requirement selected by the policy, when applicable.
116    pub const fn requirement(self) -> Option<OpenBaoVersionRequirement> {
117        self.requirement
118    }
119
120    /// Stable version returned by `/sys/health`, when a probe was performed.
121    pub const fn detected_version(self) -> Option<OpenBaoVersion> {
122        self.detected_version
123    }
124
125    /// Exact immutable profile selected for request compatibility decisions.
126    pub const fn profile_version(self) -> Option<OpenBaoVersion> {
127        self.profile_version
128    }
129
130    pub(crate) const fn unverified() -> Self {
131        Self {
132            status: OpenBaoCompatibilityStatus::Unverified,
133            policy: None,
134            requirement: None,
135            detected_version: None,
136            profile_version: None,
137        }
138    }
139
140    pub(crate) const fn verified(
141        policy: OpenBaoCompatibilityPolicyKind,
142        detected_version: OpenBaoVersion,
143        requirement: Option<OpenBaoVersionRequirement>,
144    ) -> Self {
145        Self {
146            status: OpenBaoCompatibilityStatus::Verified,
147            policy: Some(policy),
148            requirement,
149            detected_version: Some(detected_version),
150            profile_version: Some(detected_version),
151        }
152    }
153
154    pub(crate) const fn assumed(version: OpenBaoVersion) -> Self {
155        Self {
156            status: OpenBaoCompatibilityStatus::Assumed,
157            policy: Some(OpenBaoCompatibilityPolicyKind::Assumed),
158            requirement: Some(OpenBaoVersionRequirement::exact(version)),
159            detected_version: None,
160            profile_version: Some(version),
161        }
162    }
163
164    pub(crate) const fn acknowledged_unknown_newer(
165        detected_version: OpenBaoVersion,
166        profile_version: OpenBaoVersion,
167    ) -> Self {
168        Self {
169            status: OpenBaoCompatibilityStatus::AcknowledgedUnknownNewer,
170            policy: Some(OpenBaoCompatibilityPolicyKind::AcknowledgedUnknownNewer),
171            requirement: None,
172            detected_version: Some(detected_version),
173            profile_version: Some(profile_version),
174        }
175    }
176}
177
178/// Exact stable OpenBao server version.
179///
180/// Text parsing accepts only canonical `major.minor.patch` syntax with ASCII
181/// decimal components. Prefixes such as `v`, leading zeroes, whitespace,
182/// prerelease identifiers, and build metadata are rejected.
183#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
184pub struct OpenBaoVersion {
185    major: u32,
186    minor: u32,
187    patch: u32,
188}
189
190impl OpenBaoVersion {
191    /// Creates an exact stable version from numeric components.
192    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
193        Self {
194            major,
195            minor,
196            patch,
197        }
198    }
199
200    /// Major version component.
201    pub const fn major(self) -> u32 {
202        self.major
203    }
204
205    /// Minor version component.
206    pub const fn minor(self) -> u32 {
207        self.minor
208    }
209
210    /// Patch version component.
211    pub const fn patch(self) -> u32 {
212        self.patch
213    }
214}
215
216impl fmt::Display for OpenBaoVersion {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
219    }
220}
221
222impl FromStr for OpenBaoVersion {
223    type Err = Error;
224
225    fn from_str(input: &str) -> Result<Self> {
226        if input.is_empty() {
227            return Err(invalid_version("version must not be empty"));
228        }
229        if input.len() > MAX_VERSION_BYTES {
230            return Err(invalid_version("version exceeds the input limit"));
231        }
232        if !input.is_ascii() {
233            return Err(invalid_version(
234                "version must contain only ASCII digits and dots",
235            ));
236        }
237
238        let mut components = input.split('.');
239        let major = components
240            .next()
241            .ok_or_else(|| invalid_version("version must contain three components"))?;
242        let minor = components
243            .next()
244            .ok_or_else(|| invalid_version("version must contain three components"))?;
245        let patch = components
246            .next()
247            .ok_or_else(|| invalid_version("version must contain three components"))?;
248        if components.next().is_some() {
249            return Err(invalid_version("version must contain three components"));
250        }
251
252        Ok(Self::new(
253            parse_component(major)?,
254            parse_component(minor)?,
255            parse_component(patch)?,
256        ))
257    }
258}
259
260/// Permitted OpenBao server version or inclusive rolling-upgrade range.
261///
262/// String parsing accepts either an exact version such as `2.5.5` or a closed
263/// range such as `2.4.0..=2.5.5`. Open-ended ranges are intentionally not
264/// supported because they cannot provide a fail-closed compatibility bound.
265#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
266pub enum OpenBaoVersionRequirement {
267    /// One exact OpenBao release.
268    Exact(OpenBaoVersion),
269    /// Closed range including both endpoints.
270    Inclusive {
271        /// Oldest permitted release.
272        minimum: OpenBaoVersion,
273        /// Newest permitted release.
274        maximum: OpenBaoVersion,
275    },
276}
277
278impl OpenBaoVersionRequirement {
279    /// Requires one exact OpenBao release.
280    pub const fn exact(version: OpenBaoVersion) -> Self {
281        Self::Exact(version)
282    }
283
284    /// Requires a non-empty inclusive release range.
285    pub fn inclusive(minimum: OpenBaoVersion, maximum: OpenBaoVersion) -> Result<Self> {
286        if minimum > maximum {
287            return Err(invalid_requirement(
288                "minimum version must not exceed maximum version",
289            ));
290        }
291        Ok(Self::Inclusive { minimum, maximum })
292    }
293
294    /// Oldest version accepted by this requirement.
295    pub const fn minimum(self) -> OpenBaoVersion {
296        match self {
297            Self::Exact(version) => version,
298            Self::Inclusive { minimum, .. } => minimum,
299        }
300    }
301
302    /// Newest version accepted by this requirement.
303    pub const fn maximum(self) -> OpenBaoVersion {
304        match self {
305            Self::Exact(version) => version,
306            Self::Inclusive { maximum, .. } => maximum,
307        }
308    }
309
310    /// Returns whether `version` is inside the permitted closed range.
311    pub fn contains(self, version: OpenBaoVersion) -> bool {
312        version_in_closed_interval(version, self.minimum(), self.maximum())
313    }
314}
315
316pub(crate) fn version_in_closed_interval(
317    version: OpenBaoVersion,
318    minimum: OpenBaoVersion,
319    maximum: OpenBaoVersion,
320) -> bool {
321    version >= minimum && version <= maximum
322}
323
324impl fmt::Display for OpenBaoVersionRequirement {
325    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
326        match self {
327            Self::Exact(version) => version.fmt(formatter),
328            Self::Inclusive { minimum, maximum } => {
329                write!(formatter, "{minimum}..={maximum}")
330            }
331        }
332    }
333}
334
335impl FromStr for OpenBaoVersionRequirement {
336    type Err = Error;
337
338    fn from_str(input: &str) -> Result<Self> {
339        if input.is_empty() {
340            return Err(invalid_requirement("requirement must not be empty"));
341        }
342        if input.len() > MAX_REQUIREMENT_BYTES {
343            return Err(invalid_requirement("requirement exceeds the input limit"));
344        }
345
346        if let Some((minimum, maximum)) = input.split_once("..=") {
347            if minimum.is_empty() || maximum.is_empty() || maximum.contains("..=") {
348                return Err(invalid_requirement(
349                    "requirement must be an exact version or one inclusive range",
350                ));
351            }
352            return Self::inclusive(
353                parse_requirement_version(minimum)?,
354                parse_requirement_version(maximum)?,
355            );
356        }
357
358        Ok(Self::exact(parse_requirement_version(input)?))
359    }
360}
361
362impl OpenBaoCompatibilityPolicy {
363    /// Detects the server and requires one exact runtime-approved release.
364    pub fn exact(version: OpenBaoVersion) -> Result<Self> {
365        require_routable_profile(version)?;
366        Ok(Self {
367            value: OpenBaoCompatibilityPolicyValue::Exact(version),
368        })
369    }
370
371    /// Detects the server and requires a version inside an approved closed range.
372    ///
373    /// Both range endpoints must be exact releases in the runtime-approved inventory.
374    pub fn range(requirement: OpenBaoVersionRequirement) -> Result<Self> {
375        require_routable_profile(requirement.minimum())?;
376        require_routable_profile(requirement.maximum())?;
377        Ok(Self {
378            value: OpenBaoCompatibilityPolicyValue::Range(requirement),
379        })
380    }
381
382    /// Detects the server and rejects versions absent from the approved inventory.
383    #[must_use]
384    pub const fn automatic_strict() -> Self {
385        Self {
386            value: OpenBaoCompatibilityPolicyValue::AutomaticStrict,
387        }
388    }
389
390    /// Selects one runtime-approved profile without querying `/sys/health`.
391    ///
392    /// Reports produced by this policy are always marked `Assumed`, never
393    /// `Verified`. Use this only where an authenticated proxy blocks the public
394    /// health endpoint and deployment configuration supplies the exact version.
395    pub fn assume(version: OpenBaoVersion) -> Result<Self> {
396        require_routable_profile(version)?;
397        Ok(Self {
398            value: OpenBaoCompatibilityPolicyValue::Assumed(version),
399        })
400    }
401
402    /// Detects the server and tolerates versions newer than the approved inventory.
403    ///
404    /// Unknown older versions and unpublished versions inside the approved
405    /// range still fail closed. Newer versions select the latest promoted
406    /// profile and are reported as acknowledged rather than verified.
407    #[must_use]
408    pub const fn automatic_allow_unknown_newer(
409        _acknowledgement: UnknownNewerOpenBaoAcknowledgement,
410    ) -> Self {
411        Self {
412            value: OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer,
413        }
414    }
415
416    /// Public classification of this policy.
417    pub const fn kind(self) -> OpenBaoCompatibilityPolicyKind {
418        match self.value {
419            OpenBaoCompatibilityPolicyValue::Exact(_) => OpenBaoCompatibilityPolicyKind::Exact,
420            OpenBaoCompatibilityPolicyValue::Range(_) => OpenBaoCompatibilityPolicyKind::Range,
421            OpenBaoCompatibilityPolicyValue::AutomaticStrict => {
422                OpenBaoCompatibilityPolicyKind::AutomaticStrict
423            }
424            OpenBaoCompatibilityPolicyValue::Assumed(_) => OpenBaoCompatibilityPolicyKind::Assumed,
425            OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer => {
426                OpenBaoCompatibilityPolicyKind::AcknowledgedUnknownNewer
427            }
428        }
429    }
430
431    /// Exact or closed version requirement carried by this policy.
432    pub const fn requirement(self) -> Option<OpenBaoVersionRequirement> {
433        match self.value {
434            OpenBaoCompatibilityPolicyValue::Exact(version)
435            | OpenBaoCompatibilityPolicyValue::Assumed(version) => {
436                Some(OpenBaoVersionRequirement::exact(version))
437            }
438            OpenBaoCompatibilityPolicyValue::Range(requirement) => Some(requirement),
439            OpenBaoCompatibilityPolicyValue::AutomaticStrict
440            | OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer => None,
441        }
442    }
443
444    pub(crate) fn immediate_report(self) -> Option<OpenBaoCompatibilityReport> {
445        match self.value {
446            OpenBaoCompatibilityPolicyValue::Assumed(version) => {
447                Some(OpenBaoCompatibilityReport::assumed(version))
448            }
449            _ => None,
450        }
451    }
452
453    pub(crate) fn evaluate_detected(
454        self,
455        detected: OpenBaoVersion,
456    ) -> core::result::Result<OpenBaoCompatibilityReport, OpenBaoCompatibilityFailure> {
457        match self.value {
458            OpenBaoCompatibilityPolicyValue::Exact(expected) => {
459                if detected != expected {
460                    return Err(OpenBaoCompatibilityFailure::VersionMismatch {
461                        detected,
462                        requirement: OpenBaoVersionRequirement::exact(expected),
463                    });
464                }
465                Ok(OpenBaoCompatibilityReport::verified(
466                    self.kind(),
467                    detected,
468                    self.requirement(),
469                ))
470            }
471            OpenBaoCompatibilityPolicyValue::Range(requirement) => {
472                if !requirement.contains(detected) {
473                    return Err(OpenBaoCompatibilityFailure::VersionMismatch {
474                        detected,
475                        requirement,
476                    });
477                }
478                if !is_routable_profile(detected) {
479                    return Err(OpenBaoCompatibilityFailure::UnknownVersion(detected));
480                }
481                Ok(OpenBaoCompatibilityReport::verified(
482                    self.kind(),
483                    detected,
484                    self.requirement(),
485                ))
486            }
487            OpenBaoCompatibilityPolicyValue::AutomaticStrict => {
488                if !is_routable_profile(detected) {
489                    return Err(OpenBaoCompatibilityFailure::UnknownVersion(detected));
490                }
491                Ok(OpenBaoCompatibilityReport::verified(
492                    self.kind(),
493                    detected,
494                    self.requirement(),
495                ))
496            }
497            OpenBaoCompatibilityPolicyValue::Assumed(version) => {
498                Ok(OpenBaoCompatibilityReport::assumed(version))
499            }
500            OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer => {
501                if is_routable_profile(detected) {
502                    return Ok(OpenBaoCompatibilityReport::verified(
503                        self.kind(),
504                        detected,
505                        self.requirement(),
506                    ));
507                }
508                let Some(latest) = latest_routable_profile() else {
509                    return Err(OpenBaoCompatibilityFailure::UnknownVersion(detected));
510                };
511                if detected > latest {
512                    return Ok(OpenBaoCompatibilityReport::acknowledged_unknown_newer(
513                        detected, latest,
514                    ));
515                }
516                Err(OpenBaoCompatibilityFailure::UnknownVersion(detected))
517            }
518        }
519    }
520}
521
522/// HTTP or protocol method recorded for a documented OpenBao operation.
523///
524/// `Acme` and `Scan` are documentation-level protocol operations rather than
525/// methods accepted by `reqwest::Method`. This registry reports evidence only;
526/// it does not transmit requests.
527#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
528pub enum OpenBaoHttpMethod {
529    /// ACME protocol flow rooted at an OpenBao directory URL.
530    Acme,
531    /// HTTP `DELETE`.
532    Delete,
533    /// HTTP `GET`.
534    Get,
535    /// HTTP `HEAD`.
536    Head,
537    /// OpenBao `LIST` method.
538    List,
539    /// HTTP `PATCH`.
540    Patch,
541    /// HTTP `POST`.
542    Post,
543    /// HTTP `PUT`.
544    Put,
545    /// OpenBao `SCAN` method.
546    Scan,
547}
548
549impl OpenBaoHttpMethod {
550    /// Returns the documented method spelling.
551    pub const fn as_str(self) -> &'static str {
552        match self {
553            Self::Acme => "ACME",
554            Self::Delete => "DELETE",
555            Self::Get => "GET",
556            Self::Head => "HEAD",
557            Self::List => "LIST",
558            Self::Patch => "PATCH",
559            Self::Post => "POST",
560            Self::Put => "PUT",
561            Self::Scan => "SCAN",
562        }
563    }
564}
565
566/// Current crate implementation disposition attached to a stable operation identity.
567#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
568pub enum OpenBaoOperationDisposition {
569    /// The operation has an ungated typed helper.
570    Typed,
571    /// The operation has a typed helper behind its documented feature gate.
572    TypedGated,
573    /// Crate security policy blocks this operation regardless of server documentation.
574    SecurityBlocked,
575}
576
577/// Immutable evidence supporting one exact-release capability range.
578#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
579pub enum OpenBaoCapabilityEvidence {
580    /// The route is not documented in this exact-release range.
581    None,
582    /// The route appears in exact tagged OpenBao API documentation.
583    TaggedDocumentation,
584    /// The route appears in an immutable exact-release OpenAPI snapshot.
585    LockedOpenApi,
586    /// The route is present in the corrected exact 2.5.5 contract extraction.
587    CorrectedCurrentContract,
588}
589
590/// SDK reporting result for one operation on one exact compatibility profile.
591#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
592pub enum OpenBaoCapabilityAvailability {
593    /// Exact tagged evidence documents the route. This does not by itself prove SDK support.
594    DocumentedRoute,
595    /// The route is not documented for this exact locked release.
596    NotDocumented,
597    /// The server documents the route, but crate security policy blocks its use.
598    SecurityBlocked,
599}
600
601/// One complete, non-overlapping exact-release range for an operation.
602#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
603pub struct OpenBaoCapabilityRange {
604    minimum: OpenBaoVersion,
605    maximum: OpenBaoVersion,
606    evidence: OpenBaoCapabilityEvidence,
607}
608
609impl OpenBaoCapabilityRange {
610    pub(crate) const fn generated(
611        minimum: OpenBaoVersion,
612        maximum: OpenBaoVersion,
613        evidence: OpenBaoCapabilityEvidence,
614    ) -> Self {
615        Self {
616            minimum,
617            maximum,
618            evidence,
619        }
620    }
621
622    /// Oldest exact release covered by this range.
623    pub const fn minimum(self) -> OpenBaoVersion {
624        self.minimum
625    }
626
627    /// Newest exact release covered by this range.
628    pub const fn maximum(self) -> OpenBaoVersion {
629        self.maximum
630    }
631
632    /// Evidence attached to this range.
633    pub const fn evidence(self) -> OpenBaoCapabilityEvidence {
634        self.evidence
635    }
636
637    fn contains(self, version: OpenBaoVersion) -> bool {
638        version_in_closed_interval(version, self.minimum, self.maximum)
639    }
640}
641
642/// Stable, secret-free identity and route template for one OpenBao operation.
643///
644/// Path values are documentation templates such as `/sys/health` or
645/// `/:secret-mount-path/data/:path`. They never contain a caller's concrete
646/// mount, secret path, lease identifier, token accessor, or query value.
647#[derive(Clone, Copy, Debug)]
648pub struct OpenBaoOperation {
649    id: &'static str,
650    method: OpenBaoHttpMethod,
651    path_template: &'static str,
652    disposition: OpenBaoOperationDisposition,
653    ranges: &'static [OpenBaoCapabilityRange],
654}
655
656impl OpenBaoOperation {
657    const fn generated(
658        id: &'static str,
659        method: OpenBaoHttpMethod,
660        path_template: &'static str,
661        disposition: OpenBaoOperationDisposition,
662        ranges: &'static [OpenBaoCapabilityRange],
663    ) -> Self {
664        Self {
665            id,
666            method,
667            path_template,
668            disposition,
669            ranges,
670        }
671    }
672
673    /// Stable generated operation identifier.
674    pub const fn id(self) -> &'static str {
675        self.id
676    }
677
678    /// Documented method for this route identity.
679    pub const fn method(self) -> OpenBaoHttpMethod {
680        self.method
681    }
682
683    /// Secret-free documented route template.
684    pub const fn path_template(self) -> &'static str {
685        self.path_template
686    }
687
688    /// Current crate review disposition.
689    pub const fn disposition(self) -> OpenBaoOperationDisposition {
690        self.disposition
691    }
692
693    /// Complete exact-release range partition for this operation.
694    pub const fn ranges(self) -> &'static [OpenBaoCapabilityRange] {
695        self.ranges
696    }
697
698    /// Reports availability for one locked exact-release profile.
699    ///
700    /// Returns `None` for versions outside the immutable profile inventory,
701    /// including unpublished patch numbers that happen to fall numerically
702    /// between two locked releases.
703    pub fn availability(self, version: OpenBaoVersion) -> Option<OpenBaoCapabilityAvailability> {
704        if !is_generated_profile(version) {
705            return None;
706        }
707        let range = select_capability_range(self.ranges, version)?;
708        if range.evidence == OpenBaoCapabilityEvidence::None {
709            return Some(OpenBaoCapabilityAvailability::NotDocumented);
710        }
711        if self.disposition == OpenBaoOperationDisposition::SecurityBlocked {
712            return Some(OpenBaoCapabilityAvailability::SecurityBlocked);
713        }
714        Some(OpenBaoCapabilityAvailability::DocumentedRoute)
715    }
716
717    /// Returns the evidence for one locked exact-release profile.
718    pub fn evidence(self, version: OpenBaoVersion) -> Option<OpenBaoCapabilityEvidence> {
719        if !is_generated_profile(version) {
720            return None;
721        }
722        select_capability_range(self.ranges, version).map(OpenBaoCapabilityRange::evidence)
723    }
724}
725
726pub(crate) fn select_capability_range(
727    ranges: &[OpenBaoCapabilityRange],
728    version: OpenBaoVersion,
729) -> Option<OpenBaoCapabilityRange> {
730    ranges.iter().copied().find(|range| range.contains(version))
731}
732
733/// Read-only operation status in one exact OpenBao profile.
734#[derive(Clone, Copy, Debug)]
735pub struct OpenBaoCapabilityStatus {
736    operation: OpenBaoOperation,
737    availability: OpenBaoCapabilityAvailability,
738    evidence: OpenBaoCapabilityEvidence,
739}
740
741impl OpenBaoCapabilityStatus {
742    /// Operation identity and route template.
743    pub const fn operation(self) -> OpenBaoOperation {
744        self.operation
745    }
746
747    /// Server-route availability after crate security policy is applied.
748    pub const fn availability(self) -> OpenBaoCapabilityAvailability {
749        self.availability
750    }
751
752    /// Immutable evidence for this exact profile cell.
753    pub const fn evidence(self) -> OpenBaoCapabilityEvidence {
754        self.evidence
755    }
756}
757
758/// Read-only capability report for one exact locked OpenBao release.
759#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
760pub struct OpenBaoCapabilityProfile {
761    version: OpenBaoVersion,
762}
763
764/// Internal logical SDK endpoint with one or more versioned route variants.
765#[derive(Clone, Copy, Debug)]
766pub(crate) struct OpenBaoEndpointSpec {
767    id: &'static str,
768    variants: &'static [OpenBaoEndpointVariant],
769}
770
771impl OpenBaoEndpointSpec {
772    #[allow(dead_code)]
773    pub(crate) const fn new(id: &'static str, variants: &'static [OpenBaoEndpointVariant]) -> Self {
774        Self { id, variants }
775    }
776
777    pub(crate) const fn id(self) -> &'static str {
778        self.id
779    }
780
781    pub(crate) const fn variants(self) -> &'static [OpenBaoEndpointVariant] {
782        self.variants
783    }
784}
785
786/// One immutable registry operation selected over an inclusive release range.
787#[derive(Clone, Copy, Debug)]
788pub(crate) struct OpenBaoEndpointVariant {
789    operation_id: &'static str,
790    minimum: OpenBaoVersion,
791    maximum: OpenBaoVersion,
792}
793
794impl OpenBaoEndpointVariant {
795    #[allow(dead_code)]
796    pub(crate) const fn new(
797        operation_id: &'static str,
798        minimum: OpenBaoVersion,
799        maximum: OpenBaoVersion,
800    ) -> Self {
801        Self {
802            operation_id,
803            minimum,
804            maximum,
805        }
806    }
807
808    pub(crate) const fn operation_id(self) -> &'static str {
809        self.operation_id
810    }
811
812    pub(crate) const fn minimum(self) -> OpenBaoVersion {
813        self.minimum
814    }
815
816    pub(crate) const fn maximum(self) -> OpenBaoVersion {
817        self.maximum
818    }
819
820    pub(crate) fn contains(self, version: OpenBaoVersion) -> bool {
821        version >= self.minimum && version <= self.maximum
822    }
823}
824
825impl OpenBaoCapabilityProfile {
826    /// Selects an exact evidence profile, including staged candidates.
827    pub fn for_version(version: OpenBaoVersion) -> Option<Self> {
828        is_generated_profile(version).then_some(Self { version })
829    }
830
831    /// Exact OpenBao release represented by this profile.
832    pub const fn version(self) -> OpenBaoVersion {
833        self.version
834    }
835
836    /// Reports one operation by stable identifier.
837    pub fn operation(self, id: &str) -> Option<OpenBaoCapabilityStatus> {
838        let operation = openbao_operation(id)?;
839        capability_status(operation, self.version)
840    }
841
842    /// Iterates every operation and its status for this profile.
843    pub fn operations(self) -> impl Iterator<Item = OpenBaoCapabilityStatus> {
844        generated::GENERATED_OPERATIONS
845            .iter()
846            .copied()
847            .filter_map(move |operation| capability_status(operation, self.version))
848    }
849}
850
851/// Returns every stable generated operation in identifier order.
852pub fn openbao_operations() -> &'static [OpenBaoOperation] {
853    generated::GENERATED_OPERATIONS
854}
855
856/// Looks up a stable operation identifier.
857pub fn openbao_operation(id: &str) -> Option<OpenBaoOperation> {
858    generated::GENERATED_OPERATIONS
859        .binary_search_by_key(&id, |operation| operation.id())
860        .ok()
861        .map(|index| generated::GENERATED_OPERATIONS[index])
862}
863
864/// Returns all exact OpenBao releases represented by generated evidence profiles.
865///
866/// This includes staged candidates that cannot yet drive runtime policies or
867/// request dispatch.
868pub fn openbao_profile_versions() -> &'static [OpenBaoVersion] {
869    generated::GENERATED_PROFILE_VERSIONS
870}
871
872pub(crate) fn is_generated_profile(version: OpenBaoVersion) -> bool {
873    generated::GENERATED_PROFILE_VERSIONS
874        .binary_search(&version)
875        .is_ok()
876}
877
878pub(crate) fn is_routable_profile(version: OpenBaoVersion) -> bool {
879    generated::GENERATED_ROUTABLE_PROFILE_VERSIONS
880        .binary_search(&version)
881        .is_ok()
882}
883
884pub(crate) fn latest_routable_profile() -> Option<OpenBaoVersion> {
885    generated::GENERATED_ROUTABLE_PROFILE_VERSIONS
886        .last()
887        .copied()
888}
889
890fn require_routable_profile(version: OpenBaoVersion) -> Result<()> {
891    if is_routable_profile(version) {
892        Ok(())
893    } else {
894        Err(invalid_requirement(
895            "compatibility policy version is absent from the runtime-approved release inventory",
896        ))
897    }
898}
899
900fn capability_status(
901    operation: OpenBaoOperation,
902    version: OpenBaoVersion,
903) -> Option<OpenBaoCapabilityStatus> {
904    Some(OpenBaoCapabilityStatus {
905        availability: operation.availability(version)?,
906        evidence: operation.evidence(version)?,
907        operation,
908    })
909}
910
911pub(crate) mod generated {
912    use super::{
913        OpenBaoCapabilityEvidence, OpenBaoCapabilityRange, OpenBaoEndpointSpec,
914        OpenBaoEndpointVariant, OpenBaoHttpMethod, OpenBaoOperation, OpenBaoOperationDisposition,
915        OpenBaoVersion,
916    };
917
918    include!("generated/openbao_capabilities.rs");
919}
920
921fn parse_component(component: &str) -> Result<u32> {
922    if component.is_empty() {
923        return Err(invalid_version("version components must not be empty"));
924    }
925    if component.len() > 1 && component.starts_with('0') {
926        return Err(invalid_version(
927            "version components must use canonical decimal form",
928        ));
929    }
930    if !component.bytes().all(|byte| byte.is_ascii_digit()) {
931        return Err(invalid_version(
932            "version components must be decimal integers",
933        ));
934    }
935    component
936        .parse::<u32>()
937        .map_err(|_| invalid_version("version component exceeds u32"))
938}
939
940fn parse_requirement_version(input: &str) -> Result<OpenBaoVersion> {
941    input
942        .parse()
943        .map_err(|_| invalid_requirement("requirement contains an invalid stable OpenBao version"))
944}
945
946const fn invalid_version(reason: &'static str) -> Error {
947    Error::InvalidOpenBaoVersion(reason)
948}
949
950const fn invalid_requirement(reason: &'static str) -> Error {
951    Error::InvalidOpenBaoVersionRequirement(reason)
952}
953
954#[cfg(test)]
955mod tests {
956    #![allow(clippy::panic)]
957
958    use core::str::FromStr;
959
960    use super::{
961        OpenBaoCapabilityAvailability, OpenBaoCapabilityEvidence, OpenBaoCapabilityProfile,
962        OpenBaoCompatibilityFailure, OpenBaoCompatibilityPolicy, OpenBaoCompatibilityPolicyKind,
963        OpenBaoCompatibilityStatus, OpenBaoHttpMethod, OpenBaoOperationDisposition, OpenBaoVersion,
964        OpenBaoVersionRequirement, UnknownNewerOpenBaoAcknowledgement, is_routable_profile,
965        latest_routable_profile, openbao_operation, openbao_operations, openbao_profile_versions,
966    };
967    use crate::{Error, Result};
968
969    #[test]
970    fn exact_version_parsing_is_canonical_and_ordered() -> Result<()> {
971        let version = OpenBaoVersion::from_str("2.5.5")?;
972
973        assert_eq!(version.major(), 2);
974        assert_eq!(version.minor(), 5);
975        assert_eq!(version.patch(), 5);
976        assert_eq!(version.to_string(), "2.5.5");
977        assert!(OpenBaoVersion::new(2, 5, 4) < version);
978        assert!(version < OpenBaoVersion::new(3, 0, 0));
979        Ok(())
980    }
981
982    #[test]
983    fn compatibility_policies_require_runtime_approved_profiles() -> Result<()> {
984        let previous = OpenBaoVersion::new(2, 5, 5);
985        let known = OpenBaoVersion::new(2, 6, 0);
986        let unpublished = OpenBaoVersion::new(2, 4, 2);
987
988        assert_eq!(
989            OpenBaoCompatibilityPolicy::exact(known)?.kind(),
990            OpenBaoCompatibilityPolicyKind::Exact
991        );
992        assert!(OpenBaoCompatibilityPolicy::exact(previous).is_ok());
993        let approved_range = OpenBaoVersionRequirement::inclusive(previous, known)?;
994        assert!(OpenBaoCompatibilityPolicy::range(approved_range).is_ok());
995        assert!(OpenBaoCompatibilityPolicy::exact(unpublished).is_err());
996        assert!(OpenBaoCompatibilityPolicy::assume(unpublished).is_err());
997        assert!(
998            OpenBaoCompatibilityPolicy::range(OpenBaoVersionRequirement::inclusive(
999                OpenBaoVersion::new(2, 4, 1),
1000                unpublished,
1001            )?)
1002            .is_err()
1003        );
1004        Ok(())
1005    }
1006
1007    #[test]
1008    fn strict_and_unknown_newer_policies_fail_closed_or_report_acknowledgement() -> Result<()> {
1009        let strict = OpenBaoCompatibilityPolicy::automatic_strict();
1010        let unknown = OpenBaoVersion::new(2, 6, 1);
1011        assert_eq!(
1012            strict.evaluate_detected(unknown),
1013            Err(OpenBaoCompatibilityFailure::UnknownVersion(unknown))
1014        );
1015
1016        let acknowledged = OpenBaoCompatibilityPolicy::automatic_allow_unknown_newer(
1017            UnknownNewerOpenBaoAcknowledgement::acknowledge(),
1018        )
1019        .evaluate_detected(unknown)
1020        .map_err(|_| Error::Internal("acknowledged newer policy rejected a newer version"))?;
1021        assert_eq!(
1022            acknowledged.status(),
1023            OpenBaoCompatibilityStatus::AcknowledgedUnknownNewer
1024        );
1025        assert_eq!(acknowledged.detected_version(), Some(unknown));
1026        assert_eq!(
1027            acknowledged.profile_version(),
1028            Some(OpenBaoVersion::new(2, 6, 0))
1029        );
1030        Ok(())
1031    }
1032
1033    #[test]
1034    fn assumed_policy_is_never_reported_as_verified() -> Result<()> {
1035        let version = OpenBaoVersion::new(2, 2, 0);
1036        let report = OpenBaoCompatibilityPolicy::assume(version)?
1037            .immediate_report()
1038            .ok_or(Error::Internal("assumed policy did not produce a report"))?;
1039        assert_eq!(report.status(), OpenBaoCompatibilityStatus::Assumed);
1040        assert_eq!(report.detected_version(), None);
1041        assert_eq!(report.profile_version(), Some(version));
1042        assert_eq!(
1043            report.requirement(),
1044            Some(OpenBaoVersionRequirement::exact(version))
1045        );
1046        Ok(())
1047    }
1048
1049    #[test]
1050    fn version_components_accept_the_full_u32_range() -> Result<()> {
1051        let maximum = OpenBaoVersion::from_str("4294967295.4294967295.4294967295")?;
1052
1053        assert_eq!(maximum, OpenBaoVersion::new(u32::MAX, u32::MAX, u32::MAX));
1054        Ok(())
1055    }
1056
1057    #[test]
1058    fn malformed_versions_are_rejected() {
1059        for input in [
1060            "",
1061            "2",
1062            "2.5",
1063            "2.5.5.0",
1064            "v2.5.5",
1065            "2.5.5 ",
1066            " 2.5.5",
1067            "02.5.5",
1068            "2.05.5",
1069            "2.5.05",
1070            "2..5",
1071            "2.5.-1",
1072            "2.5.5-rc1",
1073            "2.5.5+vendor",
1074            "2.5.5",
1075            "4294967296.0.0",
1076        ] {
1077            assert!(
1078                input.parse::<OpenBaoVersion>().is_err(),
1079                "accepted {input:?}"
1080            );
1081        }
1082    }
1083
1084    #[test]
1085    fn version_input_is_bounded_and_errors_do_not_echo_it() {
1086        let hostile = format!("2.5.5\n{}", "9".repeat(128));
1087        let result = hostile.parse::<OpenBaoVersion>();
1088        assert!(result.is_err());
1089        let Err(error) = result else {
1090            return;
1091        };
1092        let display = error.to_string();
1093
1094        assert!(matches!(error, Error::InvalidOpenBaoVersion(_)));
1095        assert!(!display.contains('\n'));
1096        assert!(!display.contains(&hostile));
1097        assert!(display.len() < 128);
1098    }
1099
1100    #[test]
1101    fn exact_requirement_matches_only_one_release() {
1102        let version = OpenBaoVersion::new(2, 5, 5);
1103        let requirement = OpenBaoVersionRequirement::exact(version);
1104
1105        assert!(requirement.contains(version));
1106        assert!(!requirement.contains(OpenBaoVersion::new(2, 5, 4)));
1107        assert_eq!(requirement.minimum(), version);
1108        assert_eq!(requirement.maximum(), version);
1109        assert_eq!(requirement.to_string(), "2.5.5");
1110    }
1111
1112    #[test]
1113    fn inclusive_requirement_matches_both_boundaries() -> Result<()> {
1114        let minimum = OpenBaoVersion::new(2, 4, 0);
1115        let maximum = OpenBaoVersion::new(2, 5, 5);
1116        let requirement = OpenBaoVersionRequirement::inclusive(minimum, maximum)?;
1117
1118        assert!(requirement.contains(minimum));
1119        assert!(requirement.contains(OpenBaoVersion::new(2, 5, 0)));
1120        assert!(requirement.contains(maximum));
1121        assert!(!requirement.contains(OpenBaoVersion::new(2, 3, 2)));
1122        assert!(!requirement.contains(OpenBaoVersion::new(2, 5, 6)));
1123        assert_eq!(requirement.to_string(), "2.4.0..=2.5.5");
1124        Ok(())
1125    }
1126
1127    #[test]
1128    fn requirement_parser_round_trips_exact_and_range_values() -> Result<()> {
1129        for input in ["2.5.5", "2.0.0..=2.5.5", "2.5.5..=2.5.5"] {
1130            let requirement = input.parse::<OpenBaoVersionRequirement>()?;
1131            assert_eq!(requirement.to_string(), input);
1132        }
1133        Ok(())
1134    }
1135
1136    #[test]
1137    fn malformed_requirements_are_rejected_without_echoing_input() {
1138        for input in [
1139            "",
1140            "2.5",
1141            "v2.5.5",
1142            "2.5.5-rc1",
1143            "2.5.5..",
1144            "..=2.5.5",
1145            "2.5.5..=",
1146            "2.5.5..=2.5.5..=2.6.0",
1147            "2.5.5..=2.5.4",
1148            ">=2.5.5",
1149            "2.5.5..2.6.0",
1150        ] {
1151            let result = input.parse::<OpenBaoVersionRequirement>();
1152            assert!(
1153                matches!(result, Err(Error::InvalidOpenBaoVersionRequirement(_))),
1154                "wrong result for {input:?}"
1155            );
1156            if let Err(error) = result {
1157                assert!(input.is_empty() || !error.to_string().contains(input));
1158            }
1159        }
1160    }
1161
1162    #[test]
1163    fn requirement_input_is_bounded() {
1164        let input = "1".repeat(132);
1165        let result = input.parse::<OpenBaoVersionRequirement>();
1166
1167        assert!(matches!(
1168            result,
1169            Err(Error::InvalidOpenBaoVersionRequirement(
1170                "requirement exceeds the input limit"
1171            ))
1172        ));
1173    }
1174
1175    #[test]
1176    fn generated_capability_registry_is_complete_and_lookup_is_stable() {
1177        let operations = openbao_operations();
1178        let versions = openbao_profile_versions();
1179
1180        assert_eq!(operations.len(), 690);
1181        assert_eq!(versions.len(), 22);
1182        assert_eq!(versions[0], OpenBaoVersion::new(2, 0, 0));
1183        assert_eq!(versions[20], OpenBaoVersion::new(2, 5, 5));
1184        assert_eq!(versions[21], OpenBaoVersion::new(2, 6, 0));
1185        assert_eq!(
1186            latest_routable_profile(),
1187            Some(OpenBaoVersion::new(2, 6, 0))
1188        );
1189        assert!(is_routable_profile(OpenBaoVersion::new(2, 5, 5)));
1190        assert!(is_routable_profile(OpenBaoVersion::new(2, 6, 0)));
1191        assert!(OpenBaoCapabilityProfile::for_version(OpenBaoVersion::new(2, 6, 0)).is_some());
1192        assert!(OpenBaoCapabilityProfile::for_version(OpenBaoVersion::new(2, 4, 2)).is_none());
1193
1194        let mut previous = None;
1195        for operation in operations {
1196            assert!(operation.path_template().starts_with('/'));
1197            assert!(!operation.path_template().chars().any(char::is_control));
1198            if let Some(previous) = previous {
1199                assert!(previous < operation.id());
1200            }
1201            previous = Some(operation.id());
1202            assert_eq!(
1203                openbao_operation(operation.id()).map(|value| value.id()),
1204                Some(operation.id())
1205            );
1206            for version in versions {
1207                assert!(operation.availability(*version).is_some());
1208                assert!(operation.evidence(*version).is_some());
1209            }
1210        }
1211
1212        let openapi_only = operations
1213            .iter()
1214            .find(|operation| operation.path_template() == "/identity/oidc/.well-known/keys")
1215            .unwrap_or_else(|| panic!("missing locked OpenAPI-only Identity JWKS route"));
1216        assert_eq!(
1217            openapi_only.evidence(OpenBaoVersion::new(2, 5, 5)),
1218            Some(OpenBaoCapabilityEvidence::LockedOpenApi)
1219        );
1220
1221        for (method, path, disposition) in [
1222            (
1223                OpenBaoHttpMethod::Delete,
1224                "/auth/jwt/cel/role/:name",
1225                OpenBaoOperationDisposition::Typed,
1226            ),
1227            (
1228                OpenBaoHttpMethod::Get,
1229                "/auth/jwt/cel/role/:name",
1230                OpenBaoOperationDisposition::Typed,
1231            ),
1232            (
1233                OpenBaoHttpMethod::List,
1234                "/auth/jwt/cel/role",
1235                OpenBaoOperationDisposition::Typed,
1236            ),
1237            (
1238                OpenBaoHttpMethod::Patch,
1239                "/auth/jwt/cel/role/:name",
1240                OpenBaoOperationDisposition::SecurityBlocked,
1241            ),
1242            (
1243                OpenBaoHttpMethod::Post,
1244                "/auth/jwt/cel/login",
1245                OpenBaoOperationDisposition::Typed,
1246            ),
1247            (
1248                OpenBaoHttpMethod::Post,
1249                "/auth/jwt/cel/role/:name",
1250                OpenBaoOperationDisposition::Typed,
1251            ),
1252            (
1253                OpenBaoHttpMethod::Delete,
1254                "/sys/namespaces/:path/delete-sealed",
1255                OpenBaoOperationDisposition::TypedGated,
1256            ),
1257            (
1258                OpenBaoHttpMethod::Get,
1259                "/sys/namespaces/:path/seal-status",
1260                OpenBaoOperationDisposition::Typed,
1261            ),
1262            (
1263                OpenBaoHttpMethod::Post,
1264                "/sys/namespaces/:path/seal",
1265                OpenBaoOperationDisposition::TypedGated,
1266            ),
1267            (
1268                OpenBaoHttpMethod::Post,
1269                "/sys/namespaces/:path/unseal",
1270                OpenBaoOperationDisposition::TypedGated,
1271            ),
1272        ] {
1273            let operation = operations
1274                .iter()
1275                .copied()
1276                .find(|operation| operation.method() == method && operation.path_template() == path)
1277                .unwrap_or_else(|| panic!("missing OpenBao 2.6 operation {method:?} {path}"));
1278            assert_eq!(operation.disposition(), disposition);
1279            assert_eq!(
1280                operation.availability(OpenBaoVersion::new(2, 5, 5)),
1281                Some(OpenBaoCapabilityAvailability::NotDocumented)
1282            );
1283            let expected = if disposition == OpenBaoOperationDisposition::SecurityBlocked {
1284                OpenBaoCapabilityAvailability::SecurityBlocked
1285            } else {
1286                OpenBaoCapabilityAvailability::DocumentedRoute
1287            };
1288            assert_eq!(
1289                operation.availability(OpenBaoVersion::new(2, 6, 0)),
1290                Some(expected)
1291            );
1292        }
1293    }
1294
1295    #[test]
1296    fn generated_root_token_routes_are_gapless_and_profile_specific() {
1297        let endpoints = [
1298            super::generated::GENERATED_SYS_GENERATE_ROOT_CANCEL,
1299            super::generated::GENERATED_SYS_GENERATE_ROOT_START,
1300            super::generated::GENERATED_SYS_GENERATE_ROOT_STATUS,
1301            super::generated::GENERATED_SYS_GENERATE_ROOT_UPDATE,
1302        ];
1303
1304        for endpoint in endpoints {
1305            let variants = endpoint.variants();
1306            assert_eq!(variants.len(), 2);
1307            assert_eq!(variants[0].minimum(), OpenBaoVersion::new(2, 0, 0));
1308            assert_eq!(variants[0].maximum(), OpenBaoVersion::new(2, 5, 5));
1309            assert_eq!(variants[1].minimum(), OpenBaoVersion::new(2, 6, 0));
1310            assert_eq!(variants[1].maximum(), OpenBaoVersion::new(2, 6, 0));
1311            assert_ne!(variants[0].operation_id(), variants[1].operation_id());
1312            for (variant, version) in [
1313                (variants[0], OpenBaoVersion::new(2, 5, 5)),
1314                (variants[1], OpenBaoVersion::new(2, 6, 0)),
1315            ] {
1316                let operation = openbao_operation(variant.operation_id())
1317                    .unwrap_or_else(|| panic!("missing root-generation operation"));
1318                assert_eq!(
1319                    operation.availability(version),
1320                    Some(OpenBaoCapabilityAvailability::DocumentedRoute)
1321                );
1322            }
1323        }
1324    }
1325
1326    #[test]
1327    fn generated_profiles_preserve_removed_and_feature_gated_routes() {
1328        let historical = openbao_operations()
1329            .iter()
1330            .copied()
1331            .find(|operation| operation.path_template() == "/sys/internal/ui/feature-flags")
1332            .unwrap_or_else(|| panic!("missing historical feature-flags operation"));
1333        assert_eq!(historical.method(), OpenBaoHttpMethod::Get);
1334        assert_eq!(
1335            historical.availability(OpenBaoVersion::new(2, 4, 4)),
1336            Some(OpenBaoCapabilityAvailability::DocumentedRoute)
1337        );
1338        assert_eq!(
1339            historical.evidence(OpenBaoVersion::new(2, 4, 4)),
1340            Some(OpenBaoCapabilityEvidence::TaggedDocumentation)
1341        );
1342        assert_eq!(
1343            historical.availability(OpenBaoVersion::new(2, 5, 5)),
1344            Some(OpenBaoCapabilityAvailability::NotDocumented)
1345        );
1346        assert_eq!(
1347            historical.disposition(),
1348            OpenBaoOperationDisposition::TypedGated
1349        );
1350
1351        let monitor = openbao_operations()
1352            .iter()
1353            .copied()
1354            .find(|operation| {
1355                operation.method() == OpenBaoHttpMethod::Get
1356                    && operation.path_template() == "/sys/monitor"
1357            })
1358            .unwrap_or_else(|| panic!("missing monitor operation"));
1359        assert_eq!(
1360            monitor.availability(OpenBaoVersion::new(2, 5, 5)),
1361            Some(OpenBaoCapabilityAvailability::DocumentedRoute)
1362        );
1363        assert_eq!(
1364            monitor.disposition(),
1365            OpenBaoOperationDisposition::TypedGated
1366        );
1367    }
1368
1369    #[test]
1370    fn profile_reporting_contains_every_operation_without_support_inference() {
1371        let profile = OpenBaoCapabilityProfile::for_version(OpenBaoVersion::new(2, 5, 5))
1372            .unwrap_or_else(|| panic!("missing current capability profile"));
1373        let statuses = profile.operations().collect::<Vec<_>>();
1374
1375        assert_eq!(statuses.len(), openbao_operations().len());
1376        assert_eq!(profile.version(), OpenBaoVersion::new(2, 5, 5));
1377        assert!(statuses.iter().any(|status| {
1378            status.operation().disposition() == OpenBaoOperationDisposition::Typed
1379                && status.availability() == OpenBaoCapabilityAvailability::DocumentedRoute
1380                && status.evidence() != OpenBaoCapabilityEvidence::None
1381        }));
1382    }
1383}