gitlab/api/
sudo.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use crate::api::endpoint_prelude::*;
8
9/// A `sudo` modifier that can be applied to any endpoint.
10#[derive(Debug, Clone)]
11pub struct SudoContext<'a> {
12    /// The username to use for the endpoint.
13    sudo: Cow<'a, str>,
14}
15
16impl<'a> SudoContext<'a> {
17    /// Create a new `sudo` context for API endpoints.
18    pub fn new<S>(sudo: S) -> Self
19    where
20        S: Into<Cow<'a, str>>,
21    {
22        SudoContext {
23            sudo: sudo.into(),
24        }
25    }
26
27    /// Apply the context to an endpoint.
28    pub fn apply<E>(&self, endpoint: E) -> Sudo<'a, E> {
29        Sudo {
30            endpoint,
31            sudo: self.sudo.clone(),
32        }
33    }
34}
35
36/// Query information about the API calling user.
37#[derive(Debug, Clone)]
38pub struct Sudo<'a, E> {
39    /// The endpoint to call with `sudo`.
40    endpoint: E,
41
42    /// The username to use for the endpoint.
43    sudo: Cow<'a, str>,
44}
45
46/// Create a `sudo`-elevated version of an endpoint.
47pub fn sudo<'a, E, S>(endpoint: E, sudo: S) -> Sudo<'a, E>
48where
49    S: Into<Cow<'a, str>>,
50{
51    Sudo {
52        endpoint,
53        sudo: sudo.into(),
54    }
55}
56
57impl<E> Endpoint for Sudo<'_, E>
58where
59    E: Endpoint,
60{
61    fn method(&self) -> Method {
62        self.endpoint.method()
63    }
64
65    fn endpoint(&self) -> Cow<'static, str> {
66        self.endpoint.endpoint()
67    }
68
69    fn parameters(&self) -> QueryParams<'_> {
70        let mut params = self.endpoint.parameters();
71        params.push("sudo", &self.sudo);
72        params
73    }
74
75    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
76        self.endpoint.body()
77    }
78}
79
80impl<E> Pageable for Sudo<'_, E>
81where
82    E: Pageable,
83{
84    fn use_keyset_pagination(&self) -> bool {
85        self.endpoint.use_keyset_pagination()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use http::StatusCode;
92    use serde::Deserialize;
93    use serde_json::json;
94    use url::Url;
95
96    use crate::api::endpoint_prelude::*;
97    use crate::api::{self, ApiError, Query, SudoContext};
98    use crate::test::client::{ExpectedUrl, SingleTestClient};
99
100    struct Dummy;
101
102    impl Endpoint for Dummy {
103        fn method(&self) -> Method {
104            Method::GET
105        }
106
107        fn endpoint(&self) -> Cow<'static, str> {
108            "dummy".into()
109        }
110    }
111
112    #[derive(Debug, Deserialize)]
113    struct DummyResult {
114        value: u8,
115    }
116
117    #[test]
118    fn test_gitlab_non_json_response() {
119        let endpoint = ExpectedUrl::builder()
120            .endpoint("dummy")
121            .add_query_params(&[("sudo", "user")])
122            .build()
123            .unwrap();
124        let client = SingleTestClient::new_raw(endpoint, "not json");
125
126        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
127        let err = res.unwrap_err();
128        if let ApiError::GitlabService {
129            status, ..
130        } = err
131        {
132            assert_eq!(status, StatusCode::OK);
133        } else {
134            panic!("unexpected error: {}", err);
135        }
136    }
137
138    #[test]
139    fn test_gitlab_empty_response() {
140        let endpoint = ExpectedUrl::builder()
141            .endpoint("dummy")
142            .add_query_params(&[("sudo", "user")])
143            .build()
144            .unwrap();
145        let client = SingleTestClient::new_raw(endpoint, "");
146
147        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
148        let err = res.unwrap_err();
149        if let ApiError::GitlabService {
150            status, ..
151        } = err
152        {
153            assert_eq!(status, StatusCode::OK);
154        } else {
155            panic!("unexpected error: {}", err);
156        }
157    }
158
159    #[test]
160    fn test_gitlab_error_bad_json() {
161        let endpoint = ExpectedUrl::builder()
162            .endpoint("dummy")
163            .add_query_params(&[("sudo", "user")])
164            .status(StatusCode::NOT_FOUND)
165            .build()
166            .unwrap();
167        let client = SingleTestClient::new_raw(endpoint, "");
168
169        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
170        let err = res.unwrap_err();
171        if let ApiError::GitlabService {
172            status, ..
173        } = err
174        {
175            assert_eq!(status, StatusCode::NOT_FOUND);
176        } else {
177            panic!("unexpected error: {}", err);
178        }
179    }
180
181    #[test]
182    fn test_gitlab_error_detection() {
183        let endpoint = ExpectedUrl::builder()
184            .endpoint("dummy")
185            .add_query_params(&[("sudo", "user")])
186            .status(StatusCode::NOT_FOUND)
187            .build()
188            .unwrap();
189        let client = SingleTestClient::new_json(
190            endpoint,
191            &json!({
192                "message": "dummy error message",
193            }),
194        );
195
196        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
197        let err = res.unwrap_err();
198        if let ApiError::GitlabWithStatus {
199            status,
200            msg,
201        } = err
202        {
203            assert_eq!(status, StatusCode::NOT_FOUND);
204            assert_eq!(msg, "dummy error message");
205        } else {
206            panic!("unexpected error: {}", err);
207        }
208    }
209
210    #[test]
211    fn test_gitlab_error_detection_legacy() {
212        let endpoint = ExpectedUrl::builder()
213            .endpoint("dummy")
214            .add_query_params(&[("sudo", "user")])
215            .status(StatusCode::NOT_FOUND)
216            .build()
217            .unwrap();
218        let client = SingleTestClient::new_json(
219            endpoint,
220            &json!({
221                "error": "dummy error message",
222            }),
223        );
224
225        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
226        let err = res.unwrap_err();
227        if let ApiError::GitlabWithStatus {
228            status,
229            msg,
230        } = err
231        {
232            assert_eq!(status, StatusCode::NOT_FOUND);
233            assert_eq!(msg, "dummy error message");
234        } else {
235            panic!("unexpected error: {}", err);
236        }
237    }
238
239    #[test]
240    fn test_gitlab_error_detection_unknown() {
241        let endpoint = ExpectedUrl::builder()
242            .endpoint("dummy")
243            .add_query_params(&[("sudo", "user")])
244            .status(StatusCode::NOT_FOUND)
245            .build()
246            .unwrap();
247        let err_obj = json!({
248            "bogus": "dummy error message",
249        });
250        let client = SingleTestClient::new_json(endpoint, &err_obj);
251
252        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
253        let err = res.unwrap_err();
254        if let ApiError::GitlabUnrecognizedWithStatus {
255            status,
256            obj,
257        } = err
258        {
259            assert_eq!(status, StatusCode::NOT_FOUND);
260            assert_eq!(obj, err_obj);
261        } else {
262            panic!("unexpected error: {}", err);
263        }
264    }
265
266    #[test]
267    fn test_bad_deserialization() {
268        let endpoint = ExpectedUrl::builder()
269            .endpoint("dummy")
270            .add_query_params(&[("sudo", "user")])
271            .build()
272            .unwrap();
273        let client = SingleTestClient::new_json(
274            endpoint,
275            &json!({
276                "not_value": 0,
277            }),
278        );
279
280        let res: Result<DummyResult, _> = api::sudo(Dummy, "user").query(&client);
281        let err = res.unwrap_err();
282        if let ApiError::DataType {
283            source,
284            typename,
285        } = err
286        {
287            assert_eq!(source.to_string(), "missing field `value`");
288            assert_eq!(typename, "gitlab::api::sudo::tests::DummyResult");
289        } else {
290            panic!("unexpected error: {}", err);
291        }
292    }
293
294    #[test]
295    fn test_good_deserialization() {
296        let endpoint = ExpectedUrl::builder()
297            .endpoint("dummy")
298            .add_query_params(&[("sudo", "user")])
299            .build()
300            .unwrap();
301        let client = SingleTestClient::new_json(
302            endpoint,
303            &json!({
304                "value": 0,
305            }),
306        );
307
308        let res: DummyResult = api::sudo(Dummy, "user").query(&client).unwrap();
309        assert_eq!(res.value, 0);
310    }
311
312    #[test]
313    fn test_sudo_context() {
314        let endpoint = ExpectedUrl::builder()
315            .endpoint("dummy")
316            .add_query_params(&[("sudo", "user")])
317            .build()
318            .unwrap();
319        let client = SingleTestClient::new_json(
320            endpoint,
321            &json!({
322                "value": 0,
323            }),
324        );
325        let sudo_ctx = SudoContext::new("user");
326        let endpoint = sudo_ctx.apply(Dummy);
327
328        let res: DummyResult = endpoint.query(&client).unwrap();
329        assert_eq!(res.value, 0);
330    }
331
332    #[test]
333    fn test_sudo_use_keyset_pagination() {
334        let with_keyset = api::projects::jobs::Jobs::builder()
335            .project(1)
336            .build()
337            .unwrap();
338        let without_keyset = api::projects::deploy_keys::DeployKeys::builder()
339            .project(1)
340            .build()
341            .unwrap();
342
343        assert!(api::sudo(with_keyset, "user").use_keyset_pagination());
344        assert!(!api::sudo(without_keyset, "user").use_keyset_pagination());
345    }
346
347    struct DummyWithParameters;
348
349    impl Endpoint for DummyWithParameters {
350        fn method(&self) -> Method {
351            Method::GET
352        }
353
354        fn endpoint(&self) -> Cow<'static, str> {
355            "dummy/params".into()
356        }
357
358        fn parameters(&self) -> QueryParams<'_> {
359            let mut params = QueryParams::default();
360            params.push("dummy", true);
361            params
362        }
363
364        fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
365            let mut params = FormParams::default();
366            params.push("dummy", true);
367            params.into_body()
368        }
369    }
370
371    #[test]
372    fn test_sudo_parameters() {
373        let endpoint = DummyWithParameters;
374        let sudo_endpoint = api::sudo(endpoint, "user");
375
376        let params = sudo_endpoint.parameters();
377        let mut url = Url::parse("https://example.com").unwrap();
378        params.add_to_url(&mut url);
379        itertools::assert_equal(
380            url.query_pairs(),
381            [
382                ("dummy".into(), "true".into()),
383                ("sudo".into(), "user".into()),
384            ],
385        );
386    }
387
388    #[test]
389    fn test_sudo_body() {
390        let endpoint = DummyWithParameters;
391        let sudo_endpoint = api::sudo(endpoint, "user");
392
393        let params = sudo_endpoint.body().unwrap();
394        if let Some((mime, bytes)) = params {
395            assert_eq!(mime, "application/x-www-form-urlencoded");
396            assert_eq!(bytes, b"dummy=true");
397        } else {
398            panic!("expected to have `dummy=true`");
399        }
400    }
401}