Skip to main content

liminal_server/auth_membership/
pages.rs

1use 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    /// One page of the protected channels `holder` may `action` within
10    /// `workspace` (ACCESS-006 R4): both bounds the operator's, every row
11    /// checked against the question, the continuation bound to it.
12    ///
13    /// # Errors
14    /// The exact [`MembershipRefusal`]: no verified holder, an unmapped
15    /// workspace, the authority's state, a foreign or stale continuation,
16    /// the provider's failure or refusal, or a page that is not the one
17    /// asked (over bound, foreign or duplicate row, inconsistent counts).
18    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    /// One page of the subjects who may `action` on the protected `channel`
110    /// (ACCESS-006 R4), checked as [`Self::resource_page`] is.
111    ///
112    /// # Errors
113    /// As for [`Self::resource_page`], with
114    /// [`MembershipRefusal::ChannelNotMapped`] for an unmapped channel.
115    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}