liminal_server/auth_membership/
pages.rs1use super::{
2 BTreeSet, CONTRACT_VERSION, ChannelPage, MembershipAction, MembershipGate, MembershipPage,
3 MembershipRefusal, MembershipSubject, Mode, PageCursor, PageOutcome, PageParts, PageQuestion,
4 RecipientPage, RecipientPageRequest, Resource, ResourcePageRequest, bump, bump_by,
5 continuation, encoded_bytes, page_refusal,
6};
7
8impl MembershipGate {
9 pub fn resource_page(
19 &self,
20 holder: Option<&MembershipSubject>,
21 workspace: &str,
22 action: MembershipAction,
23 after: Option<&PageCursor>,
24 ) -> Result<ChannelPage, MembershipRefusal> {
25 let holder = holder.ok_or(MembershipRefusal::HolderUnverified)?;
26 if !self.map.workspaces.contains(workspace) {
27 return Err(MembershipRefusal::WorkspaceNotMapped);
28 }
29 let (revision, generation) = self.ready()?;
30 let question = PageQuestion::Resources {
31 workspace: workspace.to_owned(),
32 subject: holder.clone(),
33 action,
34 };
35 let request = ResourcePageRequest {
36 contract: CONTRACT_VERSION,
37 log: self.served.clone(),
38 workspace: Resource {
39 kind: self.workspace_kind.clone(),
40 id: workspace.to_owned(),
41 },
42 subject: holder.to_contract(),
43 kind: self.channel_kind.clone(),
44 action: action.as_str().to_owned(),
45 at_least: revision,
46 bounds: self.bounds,
47 after: continuation(after, &question, generation)?,
48 };
49 bump(&self.counters.pages_asked);
50 let page = self
51 .provider
52 .resources(&request)
53 .map_err(|error| MembershipRefusal::Unavailable { cause: error.cause })?;
54 let parts = self.page_parts(&page, revision)?;
55 let mut channels = Vec::with_capacity(parts.rows.len());
56 let mut seen = BTreeSet::new();
57 let mut held = 0_u64;
58 let mut unmapped = 0_u64;
59 for row in parts.rows {
60 if !holder.is(&row.subject) || row.resource.kind != self.channel_kind {
61 return Err(MembershipRefusal::PageForeignRow);
62 }
63 if !seen.insert(row.resource.id.as_str()) {
64 return Err(MembershipRefusal::PageDuplicateRow);
65 }
66 let Some(channel) = self.map.by_resource.get(&row.resource.id) else {
67 unmapped = unmapped.saturating_add(1);
68 continue;
69 };
70 let placed_here = self
71 .map
72 .channels
73 .get(channel)
74 .is_some_and(|mapped| mapped.workspace.id == workspace);
75 if !placed_here {
76 return Err(MembershipRefusal::PageForeignRow);
77 }
78 if row.mode == Mode::Outright {
79 channels.push(channel.clone());
80 } else {
81 held = held.saturating_add(1);
82 }
83 }
84 let current = self.stream_fence(generation)?;
85 if parts.revision < current {
86 return Err(MembershipRefusal::RevisionStale {
87 required: current,
88 decided: Some(parts.revision),
89 });
90 }
91 Ok(ChannelPage {
92 returned: u64::try_from(parts.rows.len())
93 .map_err(|_| MembershipRefusal::PageInconsistent)?,
94 channels,
95 skipped: parts.skipped,
96 held,
97 unmapped,
98 bytes: parts.bytes,
99 revision: parts.revision,
100 complete: parts.complete,
101 next: parts.next.map(|token| PageCursor {
102 token,
103 question,
104 generation,
105 }),
106 })
107 }
108
109 pub fn recipient_page(
116 &self,
117 channel: &str,
118 action: MembershipAction,
119 after: Option<&PageCursor>,
120 ) -> Result<RecipientPage, MembershipRefusal> {
121 let mapped = self
122 .map
123 .channels
124 .get(channel)
125 .ok_or(MembershipRefusal::ChannelNotMapped)?;
126 let (revision, generation) = self.ready()?;
127 let question = PageQuestion::Recipients {
128 channel: channel.to_owned(),
129 action,
130 };
131 let request = RecipientPageRequest {
132 contract: CONTRACT_VERSION,
133 log: self.served.clone(),
134 workspace: mapped.workspace.clone(),
135 resource: mapped.resource.clone(),
136 action: action.as_str().to_owned(),
137 at_least: revision,
138 bounds: self.bounds,
139 after: continuation(after, &question, generation)?,
140 };
141 bump(&self.counters.pages_asked);
142 let page = self
143 .provider
144 .recipients(&request)
145 .map_err(|error| MembershipRefusal::Unavailable { cause: error.cause })?;
146 let parts = self.page_parts(&page, revision)?;
147 let mut recipients = Vec::with_capacity(parts.rows.len());
148 let mut seen = BTreeSet::new();
149 let mut held = 0_u64;
150 for row in parts.rows {
151 if row.resource != mapped.resource {
152 return Err(MembershipRefusal::PageForeignRow);
153 }
154 if !seen.insert((row.subject.id.as_str(), row.subject.kind.as_str())) {
155 return Err(MembershipRefusal::PageDuplicateRow);
156 }
157 if row.mode == Mode::Outright {
158 recipients.push(row.subject.clone());
159 } else {
160 held = held.saturating_add(1);
161 }
162 }
163 let current = self.stream_fence(generation)?;
164 if parts.revision < current {
165 return Err(MembershipRefusal::RevisionStale {
166 required: current,
167 decided: Some(parts.revision),
168 });
169 }
170 Ok(RecipientPage {
171 returned: u64::try_from(parts.rows.len())
172 .map_err(|_| MembershipRefusal::PageInconsistent)?,
173 recipients,
174 skipped: parts.skipped,
175 held,
176 bytes: parts.bytes,
177 revision: parts.revision,
178 complete: parts.complete,
179 next: parts.next.map(|token| PageCursor {
180 token,
181 question,
182 generation,
183 }),
184 })
185 }
186
187 pub(super) fn page_parts<'p>(
188 &self,
189 page: &'p MembershipPage,
190 at_least: u64,
191 ) -> Result<PageParts<'p>, MembershipRefusal> {
192 if page.contract != CONTRACT_VERSION {
193 return Err(MembershipRefusal::ContractUnsupported {
194 contract: page.contract,
195 });
196 }
197 if page.log != self.served {
198 return Err(MembershipRefusal::LogMismatch);
199 }
200 let (rows, returned, skipped, complete, next) = match &page.outcome {
201 PageOutcome::Refused { refusal, .. } => return Err(page_refusal(refusal)),
202 PageOutcome::Page {
203 rows,
204 returned,
205 skipped,
206 complete,
207 next,
208 } => (rows, *returned, *skipped, *complete, next),
209 };
210 let revision = page.revision.ok_or(MembershipRefusal::RevisionStale {
211 required: at_least,
212 decided: None,
213 })?;
214 if revision < at_least {
215 return Err(MembershipRefusal::RevisionStale {
216 required: at_least,
217 decided: Some(revision),
218 });
219 }
220 let count = u64::try_from(rows.len()).map_err(|_| MembershipRefusal::PageInconsistent)?;
221 bump_by(&self.counters.page_rows, count);
222 if count != returned || complete == next.is_some() {
223 return Err(MembershipRefusal::PageInconsistent);
224 }
225 let bytes = encoded_bytes(rows)?;
226 if count > u64::from(self.bounds.rows) || bytes > u64::from(self.bounds.bytes) {
227 return Err(MembershipRefusal::PageOverBound { rows: count, bytes });
228 }
229 Ok(PageParts {
230 rows,
231 skipped,
232 complete,
233 next: next.clone(),
234 revision,
235 bytes,
236 })
237 }
238}