liminal_server/auth_membership/
provider.rs1use super::{
2 ADMISSION_ROUTE, AdmissionDecision, AdmissionRequest, DECISION_ROUTE, MembershipDecision,
3 MembershipPage, MembershipRequest, RECIPIENTS_ROUTE, RESOURCES_ROUTE, RecipientPageRequest,
4 ResourcePageRequest,
5};
6
7#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
9#[error("membership provider could not be asked: {cause}")]
10pub struct ProviderError {
11 pub cause: String,
13}
14
15pub(super) fn provider_cause(cause: &str) -> ProviderError {
16 ProviderError {
17 cause: cause.to_owned(),
18 }
19}
20
21pub trait MembershipProvider: Send + Sync + std::fmt::Debug {
28 fn decide(&self, request: &MembershipRequest) -> Result<MembershipDecision, ProviderError>;
34
35 fn admission(&self, request: &AdmissionRequest) -> Result<AdmissionDecision, ProviderError>;
40
41 fn resources(&self, request: &ResourcePageRequest) -> Result<MembershipPage, ProviderError>;
46
47 fn recipients(&self, request: &RecipientPageRequest) -> Result<MembershipPage, ProviderError>;
52}
53
54pub struct HttpMembershipProvider {
61 pub(super) http: reqwest::Client,
62 pub(super) base: reqwest::Url,
63 pub(super) credential: zeroize::Zeroizing<String>,
64 pub(super) handle: tokio::runtime::Handle,
65 pub(super) max_response_bytes: usize,
66 pub(super) stop: tokio::sync::watch::Receiver<bool>,
67}
68
69impl std::fmt::Debug for HttpMembershipProvider {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 formatter
72 .debug_struct("HttpMembershipProvider")
73 .field("base", &self.base.as_str())
74 .field(
75 "credential",
76 &crate::auth_pass::ByteLength(self.credential.len()),
77 )
78 .finish_non_exhaustive()
79 }
80}
81
82impl HttpMembershipProvider {
83 pub fn new(
90 issuer: &str,
91 credential: zeroize::Zeroizing<String>,
92 handle: tokio::runtime::Handle,
93 max_response_bytes: usize,
94 stop: tokio::sync::watch::Receiver<bool>,
95 ) -> Result<Self, ProviderError> {
96 let mut base =
97 reqwest::Url::parse(issuer).map_err(|_| provider_cause("issuer_not_a_url"))?;
98 if !matches!(base.scheme(), "http" | "https")
99 || base.host_str().is_none()
100 || !base.username().is_empty()
101 || base.password().is_some()
102 || base.query().is_some()
103 || base.fragment().is_some()
104 {
105 return Err(provider_cause("issuer_not_an_http_origin"));
106 }
107 if !base.path().ends_with('/') {
108 let path = format!("{}/", base.path());
109 base.set_path(&path);
110 }
111 if credential.trim().is_empty()
112 || credential.chars().any(char::is_control)
113 || max_response_bytes == 0
114 {
115 return Err(provider_cause("membership_credential_empty"));
116 }
117 let http = reqwest::Client::builder()
119 .redirect(reqwest::redirect::Policy::none())
120 .no_proxy()
121 .pool_idle_timeout(None)
122 .pool_max_idle_per_host(0)
123 .build()
124 .map_err(|_| provider_cause("http_client_unavailable"))?;
125 Ok(Self {
126 http,
127 base,
128 credential,
129 handle,
130 max_response_bytes,
131 stop,
132 })
133 }
134
135 pub(super) fn post<Q, A>(&self, route: &str, question: &Q) -> Result<A, ProviderError>
136 where
137 Q: serde::Serialize + ?Sized,
138 A: serde::de::DeserializeOwned + Send,
139 {
140 let url = self
141 .base
142 .join(route)
143 .map_err(|_| provider_cause("membership_route_unjoinable"))?;
144 let request = self
145 .http
146 .post(url)
147 .bearer_auth(self.credential.as_str())
148 .json(question);
149 let handle = &self.handle;
150 std::thread::scope(|scope| {
151 scope
152 .spawn(move || {
153 handle.block_on(async move {
154 let mut stop = self.stop.clone();
155 if *stop.borrow() { return Err(provider_cause("membership_stopped")); }
156 tokio::select! {
157 biased;
158 _ = stop.changed() => Err(provider_cause("membership_stopped")),
159 answer = async {
160 let mut response = request
161 .send()
162 .await
163 .map_err(|_| provider_cause("lys_could_not_be_asked"))?;
164 let status = response.status();
165 if !status.is_success() {
166 return Err(ProviderError {
167 cause: format!("lys_http_status_{}", status.as_u16()),
168 });
169 }
170 let mut bytes = zeroize::Zeroizing::new(Vec::new());
171 while let Some(chunk) = response.chunk().await.map_err(|_| provider_cause("lys_could_not_be_asked"))? {
172 if chunk.len() > self.max_response_bytes.saturating_sub(bytes.len()) {
173 return Err(provider_cause("membership_answer_over_bound"));
174 }
175 bytes.extend_from_slice(&chunk);
176 }
177 serde_json::from_slice(&bytes).map_err(|_| provider_cause("membership_answer_unreadable"))
178 } => answer,
179 }
180 })
181 })
182 .join()
183 })
184 .unwrap_or_else(|_| Err(provider_cause("membership_ask_did_not_complete")))
185 }
186}
187
188impl MembershipProvider for HttpMembershipProvider {
189 fn decide(&self, request: &MembershipRequest) -> Result<MembershipDecision, ProviderError> {
190 self.post(DECISION_ROUTE, request)
191 }
192
193 fn admission(&self, request: &AdmissionRequest) -> Result<AdmissionDecision, ProviderError> {
194 self.post(ADMISSION_ROUTE, request)
195 }
196
197 fn resources(&self, request: &ResourcePageRequest) -> Result<MembershipPage, ProviderError> {
198 self.post(RESOURCES_ROUTE, request)
199 }
200
201 fn recipients(&self, request: &RecipientPageRequest) -> Result<MembershipPage, ProviderError> {
202 self.post(RECIPIENTS_ROUTE, request)
203 }
204}