Skip to main content

liminal_server/auth_membership/
refusal.rs

1use super::{
2    AtomicU64, CONTRACT_UNSUPPORTED, CONTRACT_VERSION, CURSOR_FOREIGN, HotAction, LOG_MISMATCH,
3    Mode, Ordering, ServerError,
4};
5
6/// The two membership actions. Read and post are separate: an answer about
7/// one says nothing of the other (ACCESS-006 R1).
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub enum MembershipAction {
10    /// Reading a channel: a subscription, a delivered reply, result or
11    /// request.
12    Read,
13    /// Posting on a channel: a publish or an outbound service effect.
14    Post,
15}
16
17impl MembershipAction {
18    /// The contract's action word.
19    #[must_use]
20    pub const fn as_str(self) -> &'static str {
21        match self {
22            Self::Read => "read",
23            Self::Post => "post",
24        }
25    }
26
27    /// The membership action a hot route needs: a publish and a call post,
28    /// a subscription and a served request read. A call or serve still needs
29    /// its own operation grant beside the membership; membership never
30    /// implies it.
31    #[must_use]
32    pub const fn for_hot(action: HotAction) -> Self {
33        match action {
34            HotAction::Publish | HotAction::Call => Self::Post,
35            HotAction::Subscribe | HotAction::Serve => Self::Read,
36        }
37    }
38}
39
40/// Why a protected act, delivery or page was refused.
41///
42/// Every variant has a
43/// stable [`Self::name`]; none carries a payload, a credential, a resource
44/// id or a channel name, so a refusal can cross any boundary whole.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum MembershipRefusal {
47    /// The connection proved no Lys holder, so it has no membership.
48    HolderUnverified,
49    /// The admitted pass did not name its holder's directory kind.
50    HolderKindUnknown,
51    /// The verified holder's id or kind is empty or carries control
52    /// characters.
53    HolderMalformed,
54    /// No ACCESS-005 grant authority is installed, so no membership is
55    /// current.
56    AuthorityMissing,
57    /// The grant authority has not established its verified baseline.
58    NotReady,
59    /// The grant authority or the provider could not be asked, by cause.
60    Unavailable {
61        /// The stable cause, naming no response body.
62        cause: String,
63    },
64    /// The grant authority's change feed has an uncovered interval.
65    Gap {
66        /// The last revision applied before the gap.
67        after: u64,
68        /// How many revisions are missing.
69        missing: u64,
70    },
71    /// The grant log was reset; its revisions no longer compare.
72    Reset,
73    /// The authority moved to a generation this index has not yet resumed.
74    ResumePending,
75    /// A change or a resume is re-deciding this binding.
76    ChangeApplying,
77    /// The answer or the authority names another grant log or epoch.
78    LogMismatch,
79    /// The provider answered under a contract this liminal does not speak.
80    ContractUnsupported {
81        /// The contract version the provider named.
82        contract: u32,
83    },
84    /// The answer echoes a request other than the one asked: a substituted
85    /// subject, resource, workspace or action.
86    AnswerSubstituted,
87    /// The answer reflects an older revision than the one required.
88    RevisionStale {
89        /// The least revision required.
90        required: u64,
91        /// The revision decided at, if any.
92        decided: Option<u64>,
93    },
94    /// The allowing scope is neither the mapped channel nor its workspace.
95    ScopeOutsideMapping,
96    /// Lys refused the membership by its stable name.
97    Refused {
98        /// The provider's stable refusal name.
99        refusal: String,
100    },
101    /// A grant reaches the act, but only by approval.
102    Held {
103        /// The approval the grant requires.
104        mode: Mode,
105    },
106    /// The index is at its configured bound; nothing was allocated.
107    CapacityExceeded {
108        /// The configured bound.
109        limit: usize,
110    },
111    /// The index lock was poisoned by a panic elsewhere.
112    IndexPoisoned,
113    /// The workspace asked about is not in the product mapping.
114    WorkspaceNotMapped,
115    /// The channel asked about is not in the product mapping.
116    ChannelNotMapped,
117    /// The continuation was issued for another question.
118    CursorForeign,
119    /// The continuation was issued at another generation or log epoch.
120    CursorStale,
121    /// A page exceeded its row or byte bound.
122    PageOverBound {
123        /// The rows the page carried.
124        rows: u64,
125        /// The encoded bytes the rows carried.
126        bytes: u64,
127    },
128    /// A page row answers another subject, kind, resource or workspace.
129    PageForeignRow,
130    /// A page listed one row twice.
131    PageDuplicateRow,
132    /// A page's counts, completion and continuation disagree.
133    PageInconsistent,
134}
135
136impl MembershipRefusal {
137    /// Whether this refusal holds rather than denies (ACCESS-006 R2): the
138    /// authority or the index is not current, so a queued protected delivery
139    /// waits, releasing nothing, rather than being retired. Every other
140    /// refusal is a decided deny.
141    #[must_use]
142    pub const fn holds(&self) -> bool {
143        matches!(
144            self,
145            Self::AuthorityMissing
146                | Self::NotReady
147                | Self::Unavailable { .. }
148                | Self::Gap { .. }
149                | Self::Reset
150                | Self::ResumePending
151                | Self::ChangeApplying
152                | Self::IndexPoisoned
153        )
154    }
155
156    /// The stable machine name.
157    #[must_use]
158    pub fn name(&self) -> &str {
159        match self {
160            Self::HolderUnverified => "membership_holder_unverified",
161            Self::HolderKindUnknown => "membership_holder_kind_unknown",
162            Self::HolderMalformed => "membership_holder_malformed",
163            Self::AuthorityMissing => "membership_authority_missing",
164            Self::NotReady => "membership_not_ready",
165            Self::Unavailable { .. } => "membership_unavailable",
166            Self::Gap { .. } => "membership_gap",
167            Self::Reset => "membership_log_reset",
168            Self::ResumePending => "membership_resume_pending",
169            Self::ChangeApplying => "membership_change_applying",
170            Self::LogMismatch => LOG_MISMATCH,
171            Self::ContractUnsupported { .. } => CONTRACT_UNSUPPORTED,
172            Self::AnswerSubstituted => "membership_answer_substituted",
173            Self::RevisionStale { .. } => "membership_revision_stale",
174            Self::ScopeOutsideMapping => "membership_scope_outside_mapping",
175            Self::Refused { refusal } => refusal,
176            Self::Held { .. } => "membership_held",
177            Self::CapacityExceeded { .. } => "membership_capacity_exceeded",
178            Self::IndexPoisoned => "membership_index_poisoned",
179            Self::WorkspaceNotMapped => "membership_workspace_not_mapped",
180            Self::ChannelNotMapped => "membership_channel_not_mapped",
181            Self::CursorForeign => CURSOR_FOREIGN,
182            Self::CursorStale => "membership_cursor_stale",
183            Self::PageOverBound { .. } => "membership_page_over_bound",
184            Self::PageForeignRow => "membership_page_foreign_row",
185            Self::PageDuplicateRow => "membership_page_duplicate_row",
186            Self::PageInconsistent => "membership_page_inconsistent",
187        }
188    }
189}
190
191impl std::fmt::Display for MembershipRefusal {
192    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            Self::Unavailable { cause } => write!(formatter, "{}: {cause}", self.name()),
195            Self::Gap { after, missing } => write!(
196                formatter,
197                "{}: {missing} revisions missing after {after}",
198                self.name()
199            ),
200            Self::ContractUnsupported { contract } => write!(
201                formatter,
202                "{}: contract {contract}; this liminal speaks {CONTRACT_VERSION}",
203                self.name()
204            ),
205            Self::RevisionStale { required, decided } => write!(
206                formatter,
207                "{}: required {required}, decided {decided:?}",
208                self.name()
209            ),
210            Self::Held { mode } => write!(formatter, "{}: {mode:?}", self.name()),
211            Self::CapacityExceeded { limit } => {
212                write!(formatter, "{}: limit {limit}", self.name())
213            }
214            Self::PageOverBound { rows, bytes } => {
215                write!(formatter, "{}: {rows} rows, {bytes} bytes", self.name())
216            }
217            _ => formatter.write_str(self.name()),
218        }
219    }
220}
221
222impl std::error::Error for MembershipRefusal {}
223
224pub(super) fn malformed(value: &str) -> bool {
225    value.is_empty() || value.chars().any(char::is_control)
226}
227
228pub(super) fn invalid(message: impl Into<String>) -> ServerError {
229    ServerError::ConfigValidation {
230        message: message.into(),
231    }
232}
233
234pub(super) fn bump(counter: &AtomicU64) {
235    counter.fetch_add(1, Ordering::Relaxed);
236}
237
238pub(super) fn bump_by(counter: &AtomicU64, count: u64) {
239    counter.fetch_add(count, Ordering::Relaxed);
240}