1pub mod attachment_settings;
3pub mod login_providers;
4pub mod people;
5pub mod problems;
6pub mod settings;
7
8use super::{ClientError, WekanClient, transport::decode_success};
9use reqwest::{
10 Method,
11 header::{ACCEPT, CONTENT_TYPE},
12};
13use secrecy::{ExposeSecret, SecretString};
14use serde::{
15 Deserialize, Deserializer, Serialize, Serializer,
16 de::{self, DeserializeOwned, MapAccess, Visitor},
17};
18use serde_json::{Number, Value};
19
20pub const MAX_SETTINGS_BYTES: usize = 50 * 1024 * 1024;
21const MAX_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
22
23pub struct PreparedSettingsUpdate<T> {
25 body: Vec<u8>,
26 request: std::marker::PhantomData<fn() -> T>,
27}
28
29#[derive(Debug, thiserror::Error)]
30#[error("serialized settings cannot be encoded within the 50 MiB limit; no request was sent")]
31pub struct SettingsEncodeError;
32
33impl<T: Serialize> PreparedSettingsUpdate<T> {
34 pub fn new(request: &T) -> Result<Self, SettingsEncodeError> {
35 Ok(Self {
36 body: encode_bounded(request, MAX_SETTINGS_BYTES)?,
37 request: std::marker::PhantomData,
38 })
39 }
40}
41
42pub(super) fn present<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
43 d: D,
44) -> Result<Option<T>, D::Error> {
45 T::deserialize(d).map(Some)
46}
47
48pub(crate) struct Object<T>(pub T);
50impl<'de, T: Deserialize<'de>> Deserialize<'de> for Object<T> {
51 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
52 struct ObjectVisitor<T>(std::marker::PhantomData<T>);
53 impl<'de, T: Deserialize<'de>> Visitor<'de> for ObjectVisitor<T> {
54 type Value = Object<T>;
55 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
56 f.write_str("a JSON object")
57 }
58 fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
59 T::deserialize(de::value::MapAccessDeserializer::new(map)).map(Object)
60 }
61 }
62 d.deserialize_map(ObjectVisitor(std::marker::PhantomData))
63 }
64}
65pub(super) fn present_object<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
66 d: D,
67) -> Result<Option<T>, D::Error> {
68 Object::<T>::deserialize(d).map(|v| Some(v.0))
69}
70pub(super) fn nullable_object<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
71 d: D,
72) -> Result<Option<T>, D::Error> {
73 Option::<Object<T>>::deserialize(d).map(|v| v.map(|v| v.0))
74}
75pub(super) fn date<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
76 let value = String::deserialize(d)?;
77 super::datetime::validate_response_date(&value)
78 .map_err(|()| de::Error::custom("invalid settings date"))?;
79 Ok(value)
80}
81
82macro_rules! numeric_setting {
83 ($name:ident, $min:expr, $max:expr) => {
84 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
85 #[serde(transparent)]
86 pub struct $name(Number);
87 impl<'de> Deserialize<'de> for $name {
88 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
89 let number = Number::deserialize(d)?;
90 let value = number
91 .as_f64()
92 .filter(|v| v.is_finite())
93 .ok_or_else(|| de::Error::custom("expected a finite settings number"))?;
94 let min: Option<f64> = $min;
95 let max: Option<f64> = $max;
96 if min.is_some_and(|m| value < m) || max.is_some_and(|m| value > m) {
97 return Err(de::Error::custom(
98 "settings number is outside the supported range",
99 ));
100 }
101 Ok(Self(number))
102 }
103 }
104 impl std::str::FromStr for $name {
105 type Err = &'static str;
106 fn from_str(value: &str) -> Result<Self, Self::Err> {
107 serde_json::from_str(value)
108 .map_err(|_| "expected a finite JSON number within the setting's range")
109 }
110 }
111 };
112}
113numeric_setting!(SettingNumber, None, None);
114numeric_setting!(NonNegativeNumber, Some(0.0), None);
115numeric_setting!(BatchSize, Some(1.0), Some(100.0));
116numeric_setting!(DelayMs, Some(100.0), Some(10000.0));
117numeric_setting!(CpuThreshold, Some(10.0), Some(90.0));
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct MaskedSecret;
122impl<'de> Deserialize<'de> for MaskedSecret {
123 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
124 if String::deserialize(d)?.is_empty() {
125 Ok(Self)
126 } else {
127 Err(de::Error::custom("unmasked storage credential"))
128 }
129 }
130}
131
132#[derive(Clone, Debug)]
133pub struct StorageSecret(pub(crate) SecretString);
134impl<'de> Deserialize<'de> for StorageSecret {
135 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
136 String::deserialize(d).map(|v| Self(SecretString::from(v)))
137 }
138}
139impl Serialize for StorageSecret {
140 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
141 s.serialize_str(self.0.expose_secret())
142 }
143}
144
145#[derive(Clone, Debug)]
147pub struct GcsCredentials {
148 value: SecretString,
149 object: bool,
150}
151impl GcsCredentials {
152 pub(crate) fn text(value: SecretString) -> Self {
153 Self {
154 value,
155 object: false,
156 }
157 }
158 pub(crate) fn redaction_secrets(&self) -> Vec<SecretString> {
159 fn collect(value: &Value, secrets: &mut Vec<SecretString>) {
160 match value {
161 Value::String(s) => secrets.push(SecretString::from(s.clone())),
162 Value::Object(m) => m.values().for_each(|v| collect(v, secrets)),
163 Value::Array(a) => a.iter().for_each(|v| collect(v, secrets)),
164 _ => {}
165 }
166 }
167 let mut secrets = vec![self.value.clone()];
168 if let Ok(value) = serde_json::from_str::<Value>(self.value.expose_secret()) {
169 collect(&value, &mut secrets);
170 }
171 secrets
172 }
173}
174impl<'de> Deserialize<'de> for GcsCredentials {
175 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
176 match Value::deserialize(d)? {
177 Value::String(s) => Ok(Self::text(SecretString::from(s))),
178 Value::Object(map) => Ok(Self {
179 value: SecretString::from(Value::Object(map).to_string()),
180 object: true,
181 }),
182 _ => Err(de::Error::custom(
183 "expected credential text or a credential object",
184 )),
185 }
186 }
187}
188impl Serialize for GcsCredentials {
189 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
190 if self.object {
191 serde_json::from_str::<Value>(self.value.expose_secret())
193 .map_err(serde::ser::Error::custom)?
194 .serialize(s)
195 } else {
196 s.serialize_str(self.value.expose_secret())
197 }
198 }
199}
200
201impl WekanClient {
202 async fn admin_settings<T: DeserializeOwned>(
203 &self,
204 method: Method,
205 route: &str,
206 body: Option<Vec<u8>>,
207 token: &SecretString,
208 operation: &str,
209 ) -> Result<T, ClientError> {
210 let endpoint = self
211 .server()
212 .join(route)
213 .map_err(|_| ClientError::Protocol {
214 message: "could not construct settings endpoint; no request was sent".into(),
215 diagnostic: None,
216 success_status_received: false,
217 })?;
218 let mut request = self
219 .http
220 .request(method, endpoint)
221 .header(ACCEPT, "application/json")
222 .bearer_auth(token.expose_secret());
223 if let Some(body) = body {
224 request = request.header(CONTENT_TYPE, "application/json").body(body);
225 }
226 let body = self
227 .execute_success_request_with_limit(request, MAX_RESPONSE_BYTES)
228 .await?;
229 decode_success::<Object<T>>(&body, operation, true).map(|v| v.0)
230 }
231 pub async fn global_settings(
232 &self,
233 token: &SecretString,
234 ) -> Result<settings::GlobalSettingsDocument, ClientError> {
235 self.admin_settings(
236 Method::GET,
237 "api/settings",
238 None,
239 token,
240 "admin settings get",
241 )
242 .await
243 }
244 pub async fn update_global_settings(
245 &self,
246 request: PreparedSettingsUpdate<settings::UpdateGlobalSettings>,
247 token: &SecretString,
248 ) -> Result<settings::GlobalSettingsDocument, ClientError> {
249 self.admin_settings(
250 Method::PUT,
251 "api/settings",
252 Some(request.body),
253 token,
254 "admin settings update",
255 )
256 .await
257 }
258 pub async fn attachment_settings(
259 &self,
260 token: &SecretString,
261 ) -> Result<attachment_settings::AttachmentSettingsDocument, ClientError> {
262 self.admin_settings(
263 Method::GET,
264 "api/admin/attachment-settings",
265 None,
266 token,
267 "admin attachment-settings get",
268 )
269 .await
270 }
271 pub async fn update_attachment_settings(
272 &self,
273 request: PreparedSettingsUpdate<attachment_settings::UpdateAttachmentSettings>,
274 token: &SecretString,
275 ) -> Result<attachment_settings::AttachmentSettingsDocument, ClientError> {
276 self.admin_settings(
277 Method::PUT,
278 "api/admin/attachment-settings",
279 Some(request.body),
280 token,
281 "admin attachment-settings update",
282 )
283 .await
284 }
285}
286
287fn encode_bounded<T: Serialize>(request: &T, limit: usize) -> Result<Vec<u8>, SettingsEncodeError> {
288 struct BoundedBody {
289 bytes: Vec<u8>,
290 limit: usize,
291 }
292 impl std::io::Write for BoundedBody {
293 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
294 if bytes.len() > self.limit.saturating_sub(self.bytes.len()) {
295 return Err(std::io::Error::other("settings size limit"));
296 }
297 self.bytes.extend_from_slice(bytes);
298 Ok(bytes.len())
299 }
300 fn flush(&mut self) -> std::io::Result<()> {
301 Ok(())
302 }
303 }
304 let mut body = BoundedBody {
305 bytes: Vec::new(),
306 limit,
307 };
308 serde_json::to_writer(&mut body, request).map_err(|_| SettingsEncodeError)?;
309 Ok(body.bytes)
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use std::cell::Cell;
316
317 #[test]
318 fn preparation_serializes_once_and_bounds_encoded_bytes_including_escapes() {
319 struct Counted(Cell<usize>);
320 impl Serialize for Counted {
321 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
322 self.0.set(self.0.get() + 1);
323 serializer.serialize_str("line\n")
324 }
325 }
326 let request = Counted(Cell::new(0));
327 let prepared = PreparedSettingsUpdate::new(&request).unwrap();
328 assert_eq!(request.0.get(), 1);
329 let expected = b"\"line\\n\"";
330 assert_eq!(prepared.body, expected);
331 assert_eq!(encode_bounded(&"line\n", expected.len()).unwrap(), expected);
332 assert!(encode_bounded(&"line\n", expected.len() - 1).is_err());
333 }
334}