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