Skip to main content

wekan_cli/client/
admin.rs

1//! Global administrator settings for Wekan v12.57.
2pub 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
23/// A typed settings update encoded once, before authentication or transport.
24pub 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
48/// Serde structs also accept positional arrays; API response objects must be maps.
49pub(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/// A response secret must have been masked by Wekan before decoding succeeds.
120#[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/// Credential objects are opaque request secrets, never response field maps.
146#[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            // Only validated JSON objects can construct this representation.
192            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}