Skip to main content

wekan_cli/client/
admin.rs

1//! Global administrator settings for Wekan v11.06.
2pub 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
20/// A typed settings update encoded once, before authentication or transport.
21pub 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
45/// Serde structs also accept positional arrays; settings objects must be maps.
46pub(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/// A response secret must have been masked by Wekan before decoding succeeds.
117#[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/// Credential objects are opaque request secrets, never response field maps.
143#[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            // Only validated JSON objects can construct this representation.
189            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}