1use std::fmt;
2use std::time::Duration;
3
4use async_trait::async_trait;
5
6pub type BusResult<T> = Result<T, BusError>;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum BusError {
10 Unavailable {
11 reason: String,
12 exhausted_limit: Option<String>,
13 },
14 Denied {
15 subject: String,
16 reason: String,
17 },
18 Absent {
19 resource: String,
20 },
21 Clamped {
22 clamp: String,
23 retry_after: Option<Duration>,
24 },
25}
26
27impl BusError {
28 pub fn unavailable(reason: impl Into<String>) -> Self {
29 Self::Unavailable {
30 reason: reason.into(),
31 exhausted_limit: None,
32 }
33 }
34
35 pub fn unavailable_limit(limit: impl Into<String>, reason: impl Into<String>) -> Self {
36 Self::Unavailable {
37 reason: reason.into(),
38 exhausted_limit: Some(limit.into()),
39 }
40 }
41
42 pub fn denied(subject: impl Into<String>, reason: impl Into<String>) -> Self {
43 Self::Denied {
44 subject: subject.into(),
45 reason: reason.into(),
46 }
47 }
48
49 pub fn absent(resource: impl Into<String>) -> Self {
50 Self::Absent {
51 resource: resource.into(),
52 }
53 }
54
55 pub fn clamped(clamp: impl Into<String>, retry_after: Option<Duration>) -> Self {
56 Self::Clamped {
57 clamp: clamp.into(),
58 retry_after,
59 }
60 }
61}
62
63impl fmt::Display for BusError {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Unavailable {
67 reason,
68 exhausted_limit,
69 } => match exhausted_limit {
70 Some(limit) => write!(formatter, "unavailable ({limit} exhausted): {reason}"),
71 None => write!(formatter, "unavailable: {reason}"),
72 },
73 Self::Denied { subject, reason } => {
74 write!(formatter, "denied for subject {subject}: {reason}")
75 }
76 Self::Absent { resource } => write!(formatter, "absent: {resource}"),
77 Self::Clamped { clamp, retry_after } => match retry_after {
78 Some(delay) => write!(formatter, "clamped ({clamp}); retry after {delay:?}"),
79 None => write!(formatter, "clamped: {clamp}"),
80 },
81 }
82 }
83}
84
85impl std::error::Error for BusError {}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ConnectionFailure {
89 ServerClosedThenReauthenticationRefused { subject: String, reason: String },
90 LostWithoutRefusal { reason: String },
91 WriteOnSeveredSocket,
92}
93
94pub fn map_connection_failure(failure: ConnectionFailure) -> BusError {
95 match failure {
96 ConnectionFailure::ServerClosedThenReauthenticationRefused { subject, reason } => {
97 BusError::denied(subject, reason)
98 }
99 ConnectionFailure::LostWithoutRefusal { reason } => BusError::unavailable(reason),
100 ConnectionFailure::WriteOnSeveredSocket => {
101 BusError::unavailable("write attempted on a severed connection")
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct AsyncPermissionViolation {
108 pub subject: String,
109 pub reason: String,
110}
111
112impl AsyncPermissionViolation {
113 pub fn as_denied(&self) -> BusError {
114 BusError::denied(self.subject.clone(), self.reason.clone())
115 }
116}
117
118pub fn attribute_sentinel_failure(
120 requester_error: BusError,
121 permission_violation: Option<&AsyncPermissionViolation>,
122) -> BusError {
123 permission_violation
124 .map(AsyncPermissionViolation::as_denied)
125 .unwrap_or(requester_error)
126}
127
128#[async_trait]
129pub trait PermissionViolationSource: Send {
130 async fn next_violation(&mut self) -> BusResult<Option<AsyncPermissionViolation>>;
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum CredentialSignFailure {
135 RecordMissing { record: String },
136 AuthorizationRejected { record: String, reason: String },
137}
138
139pub fn map_credential_sign_failure(failure: CredentialSignFailure) -> BusError {
140 match failure {
141 CredentialSignFailure::RecordMissing { record } => {
142 BusError::absent(format!("credential signing record {record}"))
143 }
144 CredentialSignFailure::AuthorizationRejected { record, reason } => {
145 BusError::denied(record, reason)
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn connection_boundary_uses_following_refusal_not_socket_timing() {
156 assert!(matches!(
157 map_connection_failure(ConnectionFailure::ServerClosedThenReauthenticationRefused {
158 subject: "ck.a.room".into(),
159 reason: "grant revoked".into(),
160 }),
161 BusError::Denied { .. }
162 ));
163 assert!(matches!(
164 map_connection_failure(ConnectionFailure::LostWithoutRefusal {
165 reason: "connection reset".into(),
166 }),
167 BusError::Unavailable { .. }
168 ));
169 assert!(matches!(
170 map_connection_failure(ConnectionFailure::WriteOnSeveredSocket),
171 BusError::Unavailable { .. }
172 ));
173 }
174
175 #[test]
176 fn asynchronous_permission_violation_names_offending_subject() {
177 let error = AsyncPermissionViolation {
178 subject: "ck.account.denied".into(),
179 reason: "publish permission violation".into(),
180 }
181 .as_denied();
182
183 assert!(matches!(
184 error,
185 BusError::Denied { subject, .. } if subject == "ck.account.denied"
186 ));
187 }
188
189 #[test]
190 fn signing_against_deleted_record_is_absent() {
191 let error = map_credential_sign_failure(CredentialSignFailure::RecordMissing {
192 record: "spawn/7".into(),
193 });
194
195 assert!(matches!(error, BusError::Absent { .. }));
196 }
197
198 #[test]
199 fn sentinel_permission_violation_outranks_request_timeout() {
200 let violation = AsyncPermissionViolation {
201 subject: "_INBOX.UABC.reply".into(),
202 reason: "publish permission violation".into(),
203 };
204 let attributed = attribute_sentinel_failure(
205 BusError::unavailable("request timed out"),
206 Some(&violation),
207 );
208
209 assert!(matches!(
210 attributed,
211 BusError::Denied { subject, .. } if subject == "_INBOX.UABC.reply"
212 ));
213 assert!(matches!(
214 attribute_sentinel_failure(BusError::unavailable("request timed out"), None),
215 BusError::Unavailable { .. }
216 ));
217 }
218}