Skip to main content

bacnet_objects/access_control/
accompaniment.rs

1//! Local accompaniment stage (Clause 12.34.11), separate from authorization.
2use super::accompaniment_types::*;
3use super::{AccessRightsDecision, evaluate_access_rights};
4use crate::database::ObjectDatabase;
5use crate::prelude::*;
6use bacnet_types::constructed::BACnetDeviceObjectReference;
7use bacnet_types::enums::{AccessEvent, ObjectType};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::ObjectIdentifier;
10use core::time::Duration;
11
12#[path = "accompaniment_read.rs"]
13mod read;
14#[path = "accompaniment_timing.rs"]
15mod timing;
16
17/// Evaluate local accompaniment evidence without writing objects, obtaining a
18/// clock, performing I/O, or publishing an event. Rights are recomputed in this
19/// database borrow; a caller-provided grant is never accepted as authority.
20///
21/// This is only an accompaniment stage. Authentication, credential activity,
22/// other authorization checks, pending transactions, and exactly-once event
23/// publication belong to the caller. See [`AccompanimentRequest`] for explicit
24/// timing and bounded-chain evidence; no default order or timeout is installed.
25///
26/// A direct attempt using the original credential is incorrect. A chain that
27/// returns to an earlier credential after a distinct companion is unresolved;
28/// no cyclic authorization fixed point is inferred. Missing witness rows also
29/// remain unresolved, while an explicit Waiting row can reach a verified expiry.
30/// Intents refer only to the requested original transaction, including when a
31/// child's failure invalidates its companion.
32///
33/// # Example
34/// ```
35/// use bacnet_objects::{access_control::*, database::ObjectDatabase};
36/// use bacnet_types::{
37///     constructed::{BACnetAccessRule, BACnetAssignedAccessRights},
38///     enums::ObjectType,
39///     primitives::ObjectIdentifier,
40/// };
41/// use core::time::Duration;
42/// # fn main() -> Result<(), bacnet_types::error::Error> {
43/// let id = |kind, n| ObjectIdentifier::new(kind, n).unwrap();
44/// let point = id(ObjectType::ACCESS_POINT, 1);
45/// let mut db = ObjectDatabase::new();
46/// db.add(Box::new(AccessPointObject::new(1, "entry")?))?;
47/// for n in 1..=2 {
48///     let rights_id = id(ObjectType::ACCESS_RIGHTS, n);
49///     let mut rights = AccessRightsObject::new(n, format!("rights-{n}"))?;
50///     rights.set_positive_access_rules([
51///         BACnetAccessRule::new(None, Some(point.into()), true),
52///     ])?;
53///     if n == 1 {
54///         rights.set_accompaniment(Some(id(ObjectType::ACCESS_CREDENTIAL, 2).into()))?;
55///     }
56///     let mut card = AccessCredentialObject::new(n, format!("card-{n}"))?;
57///     card.set_assigned_access_rights(vec![BACnetAssignedAccessRights {
58///         assigned_access_rights: rights_id.into(), enable: true,
59///     }])?;
60///     db.add(Box::new(rights))?;
61///     db.add(Box::new(card))?;
62/// }
63/// let original = CredentialPresentation {
64///     sequence: 1, credential: id(ObjectType::ACCESS_CREDENTIAL, 1),
65///     point, at: Duration::from_secs(20),
66/// };
67/// let companion = CredentialPresentation {
68///     sequence: 2, credential: id(ObjectType::ACCESS_CREDENTIAL, 2),
69///     point, at: Duration::from_secs(21),
70/// };
71/// let witnesses = [AccompanimentWitness {
72///     original: original.sequence,
73///     evidence: AccompanimentEvidence::Presented(companion),
74/// }];
75/// let result = evaluate_accompaniment(&db, AccompanimentRequest {
76///     original, now: companion.at, witnesses: &witnesses,
77///     timing: AccompanimentTiming {
78///         order: AccompanimentOrder::After,
79///         fallback_period: Duration::from_secs(5),
80///         deadline: AccompanimentDeadline::Inclusive,
81///     },
82///     max_links: 8,
83/// })?;
84/// assert_eq!(result.outcome, AccompanimentOutcome::Satisfied);
85/// assert_eq!(result.events.len(), 1);
86/// // The application still checks authentication and all other authorization
87/// // criteria before acting; it owns transaction state and event publication.
88/// # Ok(())
89/// # }
90/// ```
91///
92/// # Errors
93/// Original credential/point or original rights-read errors are returned from
94/// [`evaluate_access_rights`]. Unavailable accompaniment/companion evidence is an
95/// [`AccompanimentOutcome::Unresolved`] result instead.
96pub fn evaluate_accompaniment(
97    db: &ObjectDatabase,
98    request: AccompanimentRequest<'_>,
99) -> Result<AccompanimentEvaluation, Error> {
100    let original_rights =
101        evaluate_access_rights(db, request.original.credential, request.original.point)?;
102    Ok(evaluate_with_rights(db, request, original_rights))
103}
104
105/// Internal composition seam: only a rights result computed in this database
106/// borrow by a trusted sibling evaluator reaches here. No public grant input.
107pub(super) fn evaluate_with_rights(
108    db: &ObjectDatabase,
109    request: AccompanimentRequest<'_>,
110    original_rights: super::AccessRightsEvaluation,
111) -> AccompanimentEvaluation {
112    let mut result = AccompanimentEvaluation {
113        original: request.original,
114        original_rights,
115        requirement: None,
116        companions: Vec::new(),
117        outcome: AccompanimentOutcome::NotEvaluated,
118        events: Vec::new(),
119    };
120    let selected = match result.original_rights.decision {
121        AccessRightsDecision::Denied { .. } => return result,
122        AccessRightsDecision::Exempt => {
123            result.outcome = AccompanimentOutcome::NotRequired;
124            return result;
125        }
126        AccessRightsDecision::Granted { rule } => rule.access_rights,
127    };
128    result.outcome = match follow(db, &request, &mut result, selected) {
129        Ok(outcome) => outcome,
130        Err(problem) => AccompanimentOutcome::Unresolved(problem),
131    };
132    result
133}
134
135fn witness(
136    request: &AccompanimentRequest<'_>,
137    sequence: u64,
138) -> Result<AccompanimentEvidence, AccompanimentProblem> {
139    let mut entries = request
140        .witnesses
141        .iter()
142        .filter(|witness| witness.original == sequence);
143    let evidence = entries
144        .next()
145        .ok_or(AccompanimentProblem::MissingWitness {
146            presentation: sequence,
147        })?
148        .evidence;
149    if entries.next().is_some() {
150        return Err(AccompanimentProblem::AmbiguousWitness {
151            presentation: sequence,
152        });
153    }
154    Ok(evidence)
155}
156fn deny(
157    result: &mut AccompanimentEvaluation,
158    current: CredentialPresentation,
159    missing: bool,
160) -> AccompanimentOutcome {
161    let missing_original = missing && current == result.original;
162    let event = if missing_original {
163        AccessEvent::DENIED_NO_ACCOMPANIMENT
164    } else {
165        AccessEvent::DENIED_INCORRECT_ACCOMPANIMENT
166    };
167    result.events.push(AccompanimentEventIntent {
168        original: result.original,
169        access_event: event,
170        presentation: result.original,
171    });
172    if missing_original {
173        AccompanimentOutcome::DeniedNoCompanion
174    } else {
175        AccompanimentOutcome::DeniedIncorrectCompanion {
176            presentation: current.sequence,
177        }
178    }
179}
180fn follow(
181    db: &ObjectDatabase,
182    request: &AccompanimentRequest<'_>,
183    result: &mut AccompanimentEvaluation,
184    mut selected: ObjectIdentifier,
185) -> Result<AccompanimentOutcome, AccompanimentProblem> {
186    let mut current = request.original;
187    let mut path = vec![current];
188    let mut period = None;
189    let mut links = 0;
190    loop {
191        let requirement = read::requirement(db, selected)?;
192        if let Some(child) = result.companions.last_mut() {
193            child.requirement = requirement.clone();
194        } else {
195            result.requirement = requirement.clone();
196        }
197        let Some(requirement) = requirement else {
198            return Ok(if links == 0 {
199                AccompanimentOutcome::NotRequired
200            } else {
201                AccompanimentOutcome::Satisfied
202            });
203        };
204        if links >= request.max_links {
205            return Err(AccompanimentProblem::LinkLimit {
206                max_links: request.max_links,
207            });
208        }
209        let period = match period {
210            Some(period) => period,
211            None => {
212                let value = read::period(db, current.point, request.timing.fallback_period)?;
213                period = Some(value);
214                value
215            }
216        };
217        let expired = timing::expired(current, request.now, period, request.timing.deadline)?;
218        let candidate = match witness(request, current.sequence)? {
219            AccompanimentEvidence::Waiting if !expired => {
220                read::available(db, &requirement)?;
221                return Ok(AccompanimentOutcome::Waiting {
222                    presentation: current.sequence,
223                });
224            }
225            AccompanimentEvidence::DeadlineReached if !expired => {
226                return Err(timing::problem(
227                    current.sequence,
228                    AccompanimentPresentationFailure::PrematureDeadline,
229                ));
230            }
231            AccompanimentEvidence::Waiting | AccompanimentEvidence::DeadlineReached => {
232                read::available(db, &requirement)?;
233                return Ok(deny(result, current, true));
234            }
235            AccompanimentEvidence::Presented(candidate) => candidate,
236        };
237        if let Some(previous) = path
238            .iter()
239            .find(|previous| previous.sequence == candidate.sequence)
240            && *previous != candidate
241        {
242            return Err(timing::problem(
243                candidate.sequence,
244                AccompanimentPresentationFailure::ReusedSequence,
245            ));
246        }
247        if !timing::eligible(current, candidate, request, period, &path)? {
248            read::available(db, &requirement)?;
249            return Ok(deny(result, current, true));
250        }
251        if candidate.credential.object_type() != ObjectType::ACCESS_CREDENTIAL {
252            return Err(AccompanimentProblem::Reference {
253                reference: candidate.credential.into(),
254                reason: AccompanimentReferenceFailure::WrongType,
255            });
256        }
257        // Only the explicitly requested transaction publishes intents. Child
258        // dependency inspection never creates a child's BY/denial transaction.
259        if links == 0 {
260            result.events.push(AccompanimentEventIntent {
261                original: result.original,
262                access_event: AccessEvent::ACCOMPANIMENT_BY,
263                presentation: candidate,
264            });
265        }
266        read::available(db, &requirement)?;
267        if candidate.credential == current.credential {
268            return Ok(deny(result, current, false));
269        }
270        if !read::matches(db, &requirement, candidate.credential)? {
271            return Ok(deny(result, current, false));
272        }
273        // Re-entering an established dependency is unresolved even when its
274        // earlier presentation would also violate this link's order policy.
275        if path.iter().any(|previous| {
276            previous.credential == candidate.credential || previous.sequence == candidate.sequence
277        }) {
278            path.push(candidate);
279            return Err(AccompanimentProblem::CyclicRequirement { path });
280        }
281        if !timing::ordered(current, candidate, request.timing.order) {
282            return Ok(deny(result, current, false));
283        }
284        let rights =
285            evaluate_access_rights(db, candidate.credential, current.point).map_err(|error| {
286                AccompanimentProblem::Rights {
287                    presentation: candidate.sequence,
288                    detail: error.to_string(),
289                }
290            })?;
291        let decision = rights.decision;
292        result.companions.push(AccompanimentCompanion {
293            presentation: candidate,
294            rights,
295            requirement: None,
296        });
297        match decision {
298            AccessRightsDecision::Denied { .. } => return Ok(deny(result, current, false)),
299            AccessRightsDecision::Exempt => return Ok(AccompanimentOutcome::Satisfied),
300            AccessRightsDecision::Granted { rule } => selected = rule.access_rights,
301        }
302        path.push(candidate);
303        current = candidate;
304        links += 1;
305    }
306}