Skip to main content

wekan_cli/client/
admin.rs

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