Skip to main content

bacnet_objects/access_control/
authorization.rs

1//! Pure authorization composition for one authenticated local presentation.
2
3use super::*;
4use crate::database::ObjectDatabase;
5use bacnet_types::enums::{AuthorizationExemption, AuthorizationMode, ObjectType};
6
7#[path = "authorization_checks.rs"]
8mod checks;
9#[path = "authorization_credential.rs"]
10mod credential;
11#[path = "authorization_phase.rs"]
12mod phase;
13#[path = "authorization_read.rs"]
14mod read;
15#[path = "authorization_rights.rs"]
16mod rights;
17
18/// Evaluate the complete supplied local authorization context without locks,
19/// awaits, I/O, clock acquisition, or property writes (Clauses 12.31/12.35).
20///
21/// The request already represents an authenticated presentation. Every required
22/// criterion is evaluated in the explicit complete order, stopping at the first
23/// denial, pending dependency, or unresolved fact. The mode's external phase
24/// follows successful criteria. NONE returns NoAuthorization, never Granted.
25///
26/// Original and companion validity use the same supplied wall time. Status and
27/// reasons are read once each and must agree with that time's validity result;
28/// a changing object's clock is not repaired or retried into a grant. This is
29/// consistent evidence at the supplied moment, not an atomic snapshot of an
30/// arbitrary application-owned ClockReader.
31///
32/// Assigned rights and accompaniment are computed internally under this shared
33/// database borrow. Only each relied-on companion's exact authentication tuple
34/// and credential prerequisites are additionally checked; child transactions or
35/// recursive whole-controller authorization are not created.
36///
37/// PASSBACK exemption removes enforcement while preserving a known SOFT
38/// detection (Clauses 12.35.25/12.32.21). As this API's evidence contract, missing
39/// or unusable optional reporting facts retain
40/// [`AuthorizationEvaluation::passback_reporting_problem`] without blocking an
41/// exempt check. This does not define a standard-wide unknown-evidence policy;
42/// unknown facts for required criteria still stop the pass.
43///
44/// Only [`AuthorizationOutcome::Granted`] permits access. Returned root intents
45/// are deterministic for fixed reads/evidence and contain at most four events,
46/// with terminal grant/denial last. The application owns publication, exactly-once
47/// handling, pending state, counters/history, and any physical action. Existing
48/// rights/accompaniment helpers retain their independent stage-only semantics.
49///
50/// # Errors
51/// An original identifier that does not name a local Access Credential or Access
52/// Point returns OBJECT / UNKNOWN_OBJECT. Missing/malformed/unsupported required
53/// evidence is retained in an Unresolved evaluation rather than a fabricated
54/// DENIED_OTHER. Request ordering and reason priority are validated at construction.
55pub fn evaluate_authorization(
56    db: &ObjectDatabase,
57    request: AuthorizationRequest<'_>,
58) -> Result<AuthorizationEvaluation, Error> {
59    read::object(
60        db,
61        request.original.credential,
62        ObjectType::ACCESS_CREDENTIAL,
63    )?;
64    read::object(db, request.original.point, ObjectType::ACCESS_POINT)?;
65    let mut result = AuthorizationEvaluation {
66        original: request.original,
67        mode: None,
68        outcome: AuthorizationOutcome::NoAuthorization,
69        checks: Vec::new(),
70        rights: None,
71        accompaniment: None,
72        credentials: Vec::new(),
73        companion_diagnostics: Vec::new(),
74        passback_reporting_problem: None,
75        events: Vec::new(),
76    };
77    let mode = match read::mode(db, request.original.point) {
78        Ok(mode) => mode,
79        Err(problem) => {
80            result.record(
81                AuthorizationStage::Admission,
82                AuthorizationCheckOutcome::Unresolved(problem),
83            );
84            return Ok(result);
85        }
86    };
87    result.mode = Some(mode);
88    if mode == AuthorizationMode::NONE {
89        return Ok(result);
90    }
91    if request.now < request.original.at {
92        result.record(
93            AuthorizationStage::Admission,
94            AuthorizationCheckOutcome::Unresolved(AuthorizationProblem::Context(
95                "original presentation is after observation time",
96            )),
97        );
98        return Ok(result);
99    }
100    let exemptions = match read::exemptions(db, request.original.credential) {
101        Ok(exemptions) => exemptions,
102        Err(problem) => {
103            result.record(
104                AuthorizationStage::Admission,
105                AuthorizationCheckOutcome::Unresolved(problem),
106            );
107            return Ok(result);
108        }
109    };
110    for check in request.order.checks() {
111        let outcome = match *check {
112            AuthorizationCheck::Credential => {
113                credential::check(db, &request, request.original, &mut result).map(|denial| {
114                    denial.map_or(AuthorizationCheckOutcome::Passed, |(_, event)| {
115                        AuthorizationCheckOutcome::Denied(event)
116                    })
117                })
118            }
119            AuthorizationCheck::AccessRights => {
120                rights::check(db, &request, mode, &exemptions, &mut result)
121            }
122            other => checks::check(db, &request, mode, &exemptions, other, &mut result),
123        }
124        .unwrap_or_else(AuthorizationCheckOutcome::Unresolved);
125        if !result.record(AuthorizationStage::Criterion(*check), outcome) {
126            return Ok(result);
127        }
128    }
129    if let Some((stage, outcome)) = phase::check(db, &request, mode, &exemptions, &mut result)
130        && !result.record(stage, outcome)
131    {
132        return Ok(result);
133    }
134    result.outcome = AuthorizationOutcome::Granted;
135    result.event(AccessEvent::GRANTED, request.original);
136    Ok(result)
137}
138
139impl AuthorizationEvaluation {
140    fn record(&mut self, stage: AuthorizationStage, outcome: AuthorizationCheckOutcome) -> bool {
141        self.checks.push(AuthorizationCheckEvidence {
142            stage,
143            outcome: outcome.clone(),
144        });
145        match outcome {
146            AuthorizationCheckOutcome::Passed
147            | AuthorizationCheckOutcome::Exempt(_)
148            | AuthorizationCheckOutcome::NotApplicable => true,
149            AuthorizationCheckOutcome::Denied(access_event) => {
150                self.outcome = AuthorizationOutcome::Denied {
151                    stage,
152                    access_event,
153                };
154                self.event(access_event, self.original);
155                false
156            }
157            AuthorizationCheckOutcome::Pending => {
158                self.outcome = AuthorizationOutcome::Pending(stage);
159                false
160            }
161            AuthorizationCheckOutcome::Unresolved(problem) => {
162                self.outcome = AuthorizationOutcome::Unresolved { stage, problem };
163                false
164            }
165        }
166    }
167
168    fn event(&mut self, access_event: AccessEvent, presentation: CredentialPresentation) {
169        self.events.push(AuthorizationEventIntent {
170            original: self.original,
171            access_event,
172            presentation,
173        });
174    }
175}