1use super::{
2 AdmissionRequest, Arc, AuthorityState, CONTRACT_VERSION, ChannelMap, Counters, Decided,
3 DecisionIndex, DecisionKey, GrantLog, Indexed, MappedChannel, MembershipAction,
4 MembershipAuthority, MembershipConfig, MembershipCounts, MembershipProvider, MembershipRefusal,
5 MembershipRequest, MembershipSubject, Mutex, MutexGuard, OnceLock, PageBounds, Resource,
6 ServerError, admitted, bump, bump_by, channel_map, grant_stream, invalid, judged,
7 validated_bounds,
8};
9
10pub struct MembershipGate {
13 pub(super) served: GrantLog,
14 pub(super) workspace_kind: String,
15 pub(super) channel_kind: String,
16 pub(super) map: ChannelMap,
17 pub(super) bounds: PageBounds,
18 pub(super) max_decisions: usize,
19 pub(super) connect_workspace: Option<String>,
20 pub(super) provider: Arc<dyn MembershipProvider>,
21 pub(super) authority: OnceLock<Arc<dyn MembershipAuthority>>,
22 pub(super) stream: OnceLock<Arc<grant_stream::StreamState>>,
23 pub(super) index: Mutex<DecisionIndex>,
24 pub(super) counters: Counters,
25}
26
27impl std::fmt::Debug for MembershipGate {
28 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 formatter
30 .debug_struct("MembershipGate")
31 .field("served", &self.served)
32 .field("channels", &self.map.channels.len())
33 .field("bounds", &self.bounds)
34 .field("max_decisions", &self.max_decisions)
35 .field("authority", &self.authority.get().is_some())
36 .finish_non_exhaustive()
37 }
38}
39
40impl MembershipGate {
41 pub fn new(
53 config: &MembershipConfig,
54 provider: Arc<dyn MembershipProvider>,
55 ) -> Result<Self, ServerError> {
56 let bounds = validated_bounds(config)?;
57 let map = channel_map(config)?;
58 if let Some(workspace) = &config.connect_workspace
59 && !map.workspaces.contains(workspace)
60 {
61 return Err(invalid(
62 "auth.lys.membership.connect_workspace is not a mapped workspace",
63 ));
64 }
65 Ok(Self {
66 served: GrantLog {
67 identity: config.log_identity.clone(),
68 epoch: config.log_epoch,
69 },
70 workspace_kind: config.workspace_kind.clone(),
71 channel_kind: config.channel_kind.clone(),
72 map,
73 bounds,
74 max_decisions: config.max_decisions,
75 connect_workspace: config.connect_workspace.clone(),
76 provider,
77 authority: OnceLock::new(),
78 stream: OnceLock::new(),
79 index: Mutex::new(DecisionIndex::default()),
80 counters: Counters::default(),
81 })
82 }
83
84 pub fn install_authority(
90 &self,
91 authority: Arc<dyn MembershipAuthority>,
92 ) -> Result<(), Arc<dyn MembershipAuthority>> {
93 self.authority.set(authority)
94 }
95
96 pub fn install_stream_authority(&self) -> Result<Arc<grant_stream::StreamState>, ServerError> {
101 if let Some(state) = self.stream.get() {
102 return Ok(Arc::clone(state));
103 }
104 let state = grant_stream::StreamState::for_log(self.served.clone());
105 self.install_authority(state.clone())
106 .map_err(|_| invalid("membership_authority_already_installed"))?;
107 self.stream
108 .set(Arc::clone(&state))
109 .map_err(|_| invalid("membership_stream_already_installed"))?;
110 Ok(state)
111 }
112
113 #[must_use]
115 pub const fn served_log(&self) -> &GrantLog {
116 &self.served
117 }
118
119 #[must_use]
121 pub fn protects(&self, channel: &str) -> bool {
122 self.map.channels.contains_key(channel)
123 }
124
125 #[must_use]
127 pub fn mapped(&self, channel: &str) -> Option<&MappedChannel> {
128 self.map.channels.get(channel)
129 }
130
131 #[must_use]
134 pub fn channel_for_conversation(&self, conversation: u64) -> Option<&str> {
135 self.map
136 .conversations
137 .get(&conversation)
138 .map(String::as_str)
139 }
140
141 #[must_use]
143 pub fn counts(&self) -> MembershipCounts {
144 self.counters.snapshot()
145 }
146
147 pub fn retained(&self) -> Result<usize, MembershipRefusal> {
152 Ok(self.lock()?.entries.len())
153 }
154
155 pub(super) fn lock(&self) -> Result<MutexGuard<'_, DecisionIndex>, MembershipRefusal> {
156 bump(&self.counters.locks);
157 self.index
158 .lock()
159 .map_err(|_| MembershipRefusal::IndexPoisoned)
160 }
161
162 pub(super) fn ready(&self) -> Result<(u64, u64), MembershipRefusal> {
165 let authority = self
166 .authority
167 .get()
168 .ok_or(MembershipRefusal::AuthorityMissing)?;
169 match authority.state() {
170 AuthorityState::Ready {
171 log,
172 revision,
173 generation,
174 } => {
175 if log == self.served {
176 Ok((revision, generation))
177 } else {
178 Err(MembershipRefusal::LogMismatch)
179 }
180 }
181 AuthorityState::NotReady => Err(MembershipRefusal::NotReady),
182 AuthorityState::Unavailable { cause } => Err(MembershipRefusal::Unavailable { cause }),
183 AuthorityState::Gap { after, missing } => {
184 Err(MembershipRefusal::Gap { after, missing })
185 }
186 AuthorityState::Reset => Err(MembershipRefusal::Reset),
187 }
188 }
189
190 pub(super) fn stream_fence(&self, generation: u64) -> Result<u64, MembershipRefusal> {
193 let (revision, current) = self.ready()?;
194 if current != generation {
195 return Err(MembershipRefusal::ResumePending);
196 }
197 Ok(revision)
198 }
199
200 pub fn check(
211 &self,
212 pid: u64,
213 holder: Option<&MembershipSubject>,
214 channel: &str,
215 action: MembershipAction,
216 ) -> Result<Decided, MembershipRefusal> {
217 let Some(mapped) = self.map.channels.get(channel) else {
218 return Ok(Decided::NotProtected);
219 };
220 let decided = self.check_mapped(pid, holder, channel, mapped, action);
221 if decided.is_err() {
222 bump(&self.counters.refused);
223 }
224 decided
225 }
226
227 pub fn check_release(
233 &self,
234 pid: u64,
235 holder: Option<&MembershipSubject>,
236 channel: &str,
237 action: MembershipAction,
238 ) -> Result<Decided, MembershipRefusal> {
239 bump(&self.counters.release_checks);
240 self.check(pid, holder, channel, action)
241 }
242
243 pub(super) fn check_mapped(
244 &self,
245 pid: u64,
246 holder: Option<&MembershipSubject>,
247 channel: &str,
248 mapped: &MappedChannel,
249 action: MembershipAction,
250 ) -> Result<Decided, MembershipRefusal> {
251 let holder = holder.ok_or(MembershipRefusal::HolderUnverified)?;
252 let (revision, generation) = self.ready()?;
253 let key = DecisionKey {
254 subject: holder.clone(),
255 channel: channel.to_owned(),
256 action,
257 };
258 let (ticket, at_least) = {
259 let mut index = self.lock()?;
260 index.adopt(generation)?;
261 if let Some(verdict) = index.known(&key, pid) {
262 self.stream_fence(generation)?;
263 drop(index);
264 bump(&self.counters.local_lookups);
265 return verdict.result();
266 }
267 let at_least = revision.max(index.applied);
268 let ticket = index.reserve(&key, mapped, pid, self.max_decisions)?;
269 drop(index);
270 (ticket, at_least)
271 };
272 let asked = self.ask(holder, mapped, action, at_least);
273 let mut index = self.lock()?;
274 if let Err(refusal) = self.stream_fence(generation) {
275 index.forget(&key, ticket);
276 return Err(refusal);
277 }
278 match asked {
279 Ok(verdict) => {
280 let current = index.generation == Some(generation)
281 && index.settle(&key, ticket, verdict.clone());
282 if !current {
283 index.forget(&key, ticket);
286 return Err(MembershipRefusal::ChangeApplying);
287 }
288 drop(index);
289 verdict.result()
290 }
291 Err(refusal) => {
292 index.forget(&key, ticket);
293 drop(index);
294 Err(refusal)
295 }
296 }
297 }
298
299 pub(super) fn ask(
300 &self,
301 holder: &MembershipSubject,
302 mapped: &MappedChannel,
303 action: MembershipAction,
304 at_least: u64,
305 ) -> Result<Indexed, MembershipRefusal> {
306 let request = MembershipRequest {
307 contract: CONTRACT_VERSION,
308 log: self.served.clone(),
309 workspace: mapped.workspace.clone(),
310 subject: holder.to_contract(),
311 resource: mapped.resource.clone(),
312 action: action.as_str().to_owned(),
313 at_least,
314 };
315 bump(&self.counters.decisions_asked);
316 let decision = self
317 .provider
318 .decide(&request)
319 .map_err(|error| MembershipRefusal::Unavailable { cause: error.cause })?;
320 judged(&request, &self.served, mapped, &decision)
321 }
322
323 pub fn admit_connect(
332 &self,
333 holder: &MembershipSubject,
334 ) -> Result<Option<u64>, MembershipRefusal> {
335 let Some(workspace) = &self.connect_workspace else {
336 return Ok(None);
337 };
338 let (revision, generation) = self.ready()?;
339 let request = AdmissionRequest {
340 contract: CONTRACT_VERSION,
341 log: self.served.clone(),
342 workspace: Resource {
343 kind: self.workspace_kind.clone(),
344 id: workspace.clone(),
345 },
346 subject: holder.to_contract(),
347 at_least: revision,
348 };
349 bump(&self.counters.admissions_asked);
350 let decision = self
351 .provider
352 .admission(&request)
353 .map_err(|error| MembershipRefusal::Unavailable { cause: error.cause })?;
354 let admitted_at = admitted(&request, &self.served, &decision)?;
355 let current = self.stream_fence(generation)?;
356 if admitted_at < current {
357 return Err(MembershipRefusal::RevisionStale {
358 required: current,
359 decided: Some(admitted_at),
360 });
361 }
362 Ok(Some(admitted_at))
363 }
364
365 pub fn release_connection(&self, pid: u64) {
369 match self.lock() {
370 Ok(mut index) => {
371 let released = index.release(pid);
372 drop(index);
373 bump_by(&self.counters.released, released);
374 }
375 Err(refusal) => {
376 tracing::warn!(
377 pid,
378 refusal = refusal.name(),
379 "membership bindings could not be released"
380 );
381 }
382 }
383 }
384
385 pub(super) fn redecide(&self, tickets: Vec<(DecisionKey, u64)>, at_least: u64) -> (u64, u64) {
388 let mut settled = 0_u64;
389 let mut failed = 0_u64;
390 for (key, ticket) in tickets {
391 let asked = self
392 .map
393 .channels
394 .get(&key.channel)
395 .ok_or(MembershipRefusal::ChannelNotMapped)
396 .and_then(|mapped| self.ask(&key.subject, mapped, key.action, at_least));
397 let Ok(mut index) = self.lock() else {
398 failed = failed.saturating_add(1);
399 continue;
400 };
401 let kept = asked.is_ok_and(|verdict| index.settle(&key, ticket, verdict));
402 if kept {
403 settled = settled.saturating_add(1);
404 } else {
405 index.forget(&key, ticket);
406 failed = failed.saturating_add(1);
407 }
408 }
409 (settled, failed)
410 }
411}