liminal_server/
auth_membership.rs1use std::collections::{BTreeMap, BTreeSet};
43use std::sync::atomic::{AtomicU64, Ordering};
44use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
45
46use lys_pass::membership::{
47 CONTRACT_UNSUPPORTED, CONTRACT_VERSION, GrantLog, LOG_MISMATCH, MembershipDecision,
48 MembershipRequest, Subject, Verdict,
49};
50use lys_pass::membership_admission::{AdmissionDecision, AdmissionRequest};
51use lys_pass::membership_pages::{
52 CURSOR_FOREIGN, CURSOR_RESET, MembershipPage, PageBounds, PageOutcome, PageRow,
53 RecipientPageRequest, ResourcePageRequest,
54};
55use lys_pass::rights::{Mode, Resource};
56
57use crate::ServerError;
58use crate::auth_lys::HotAction;
59use crate::auth_lys::session::LysSession;
60use crate::config::types::MembershipConfig;
61
62pub use crate::agents::binding::check as check_agent_binding;
63
64pub const DECISION_ROUTE: &str = "grants/membership";
66pub const ADMISSION_ROUTE: &str = "grants/membership/admission";
68pub const RESOURCES_ROUTE: &str = "grants/membership/resources";
70pub const RECIPIENTS_ROUTE: &str = "grants/membership/recipients";
72
73mod refusal;
74pub use refusal::{MembershipAction, MembershipRefusal};
75use refusal::{bump, bump_by, invalid, malformed};
76
77mod mapping;
78use mapping::{ChannelMap, channel_map, validated_bounds};
79pub use mapping::{MappedChannel, MembershipSubject};
80
81mod provider;
82pub use provider::{HttpMembershipProvider, MembershipProvider, ProviderError};
83
84mod authority;
85use authority::Counters;
86pub use authority::{
87 AppliedChange, AuthorityState, Decided, MembershipAuthority, MembershipChange,
88 MembershipCounts, MembershipListener, ResumeReport,
89};
90
91mod index;
92pub use index::{ChannelPage, PageCursor, RecipientPage};
93use index::{DecisionIndex, DecisionKey, Indexed, PageParts, PageQuestion, resource_key};
94
95mod gate;
96pub use gate::MembershipGate;
97
98mod pages;
99
100mod listener;
101use listener::{admitted, continuation, encoded_bytes, judged, page_refusal};
102
103mod connection;
104pub use connection::{ConnectionMembership, ReleaseCheck};
105
106#[path = "auth_lys/grant_stream_owner.rs"]
107pub mod grant_stream;
108
109pub(crate) fn validate_configuration(config: &MembershipConfig) -> Result<(), ServerError> {
114 validated_bounds(config)?;
115 let map = channel_map(config)?;
116 if config
117 .connect_workspace
118 .as_ref()
119 .is_some_and(|workspace| !map.workspaces.contains(workspace))
120 {
121 return Err(invalid(
122 "auth.lys.membership.connect_workspace is not a mapped workspace",
123 ));
124 }
125 if config.credential_path.as_os_str().is_empty() {
126 return Err(invalid(
127 "auth.lys.membership.credential_path must name a file",
128 ));
129 }
130 Ok(())
131}
132
133pub fn configured_gate(
141 config: &MembershipConfig,
142 issuer: &str,
143) -> Result<Arc<MembershipGate>, ServerError> {
144 let handle = tokio::runtime::Handle::try_current()
145 .map_err(|_| invalid("auth.lys.membership needs the server's tokio runtime at start"))?;
146 configured_gate_on(config, issuer, handle)
147}
148
149pub fn configured_gate_on(
154 config: &MembershipConfig,
155 issuer: &str,
156 handle: tokio::runtime::Handle,
157) -> Result<Arc<MembershipGate>, ServerError> {
158 let response_bytes = usize::try_from(config.page_bytes)
159 .map_err(|_| invalid("membership_page_bytes_overflow"))?;
160 let credential = grant_stream::read_credential(&config.credential_path, response_bytes)?;
161 let gate_state = grant_stream::StreamState::for_log(GrantLog {
162 identity: config.log_identity.clone(),
163 epoch: config.log_epoch,
164 });
165 let provider = HttpMembershipProvider::new(
166 issuer,
167 credential,
168 handle,
169 response_bytes,
170 gate_state.cancellation(),
171 )
172 .map_err(|error| {
173 invalid(format!(
174 "auth.lys.membership provider refused: {}",
175 error.cause
176 ))
177 })?;
178 let gate = Arc::new(MembershipGate::new(config, Arc::new(provider))?);
179 gate.install_authority(gate_state.clone())
180 .map_err(|_| invalid("membership_authority_already_installed"))?;
181 gate.stream
182 .set(gate_state)
183 .map_err(|_| invalid("membership_stream_already_installed"))?;
184 Ok(gate)
185}
186
187#[cfg(any(test, feature = "test-support"))]
190pub mod fixture;
191
192#[cfg(test)]
193#[path = "auth_membership_tests.rs"]
194mod tests;