gitlab/
gitlab.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 std::any;
8use std::convert::TryInto;
9use std::fmt::{self, Debug};
10
11use async_trait::async_trait;
12use bytes::Bytes;
13use graphql_client::{GraphQLQuery, QueryBody, Response};
14use http::{HeaderMap, HeaderName, Response as HttpResponse};
15use itertools::Itertools;
16use log::{debug, info};
17use reqwest::blocking::Client;
18use reqwest::{Certificate, Client as AsyncClient};
19use serde::de::DeserializeOwned;
20use serde::Deserialize;
21use thiserror::Error;
22use url::Url;
23
24#[cfg(any(feature = "client_der", feature = "client_pem"))]
25use reqwest::Identity as TlsIdentity;
26
27use crate::api;
28use crate::auth::{Auth, AuthError};
29
30const DEFAULT_USER_AGENT: &str = concat!("rust-gitlab/v", env!("CARGO_PKG_VERSION"));
31
32/// Custom root certificate store.
33#[derive(Debug, Clone)]
34pub struct CertificateBundle {
35    certs: Vec<Certificate>,
36}
37
38impl CertificateBundle {
39    /// A `DER`-encoded certificate.
40    #[cfg(feature = "client_der")]
41    pub fn der(bytes: &[u8]) -> Result<Self, reqwest::Error> {
42        let cert = Certificate::from_der(bytes)?;
43
44        Ok(Self {
45            certs: vec![cert],
46        })
47    }
48
49    /// A `PEM`-encoded certificate.
50    #[cfg(feature = "client_pem")]
51    pub fn pem(bytes: &[u8]) -> Result<Self, reqwest::Error> {
52        let cert = Certificate::from_pem(bytes)?;
53
54        Ok(Self {
55            certs: vec![cert],
56        })
57    }
58
59    /// A `PEM`-encoded certificate bundle.
60    #[cfg(feature = "client_pem")]
61    pub fn pem_bundle(bytes: &[u8]) -> Result<Self, reqwest::Error> {
62        let certs = Certificate::from_pem_bundle(bytes)?;
63
64        Ok(Self {
65            certs,
66        })
67    }
68}
69
70#[derive(Debug, Error)]
71#[non_exhaustive]
72pub enum GitlabError {
73    #[error("failed to parse url: {}", source)]
74    UrlParse {
75        #[from]
76        source: url::ParseError,
77    },
78    #[error("error setting auth header: {}", source)]
79    AuthError {
80        #[from]
81        source: AuthError,
82    },
83    #[error("communication with gitlab: {}", source)]
84    Communication {
85        #[from]
86        source: reqwest::Error,
87    },
88    #[error("gitlab HTTP error: {}", status)]
89    Http { status: reqwest::StatusCode },
90    #[allow(clippy::upper_case_acronyms)]
91    #[error("graphql error: [\"{}\"]", message.iter().format("\", \""))]
92    GraphQL { message: Vec<graphql_client::Error> },
93    #[error("no response from gitlab")]
94    NoResponse {},
95    #[error("could not parse {} data from JSON: {}", typename, source)]
96    DataType {
97        #[source]
98        source: serde_json::Error,
99        typename: &'static str,
100    },
101    #[error("api error: {}", source)]
102    Api {
103        #[from]
104        source: api::ApiError<RestError>,
105    },
106}
107
108impl GitlabError {
109    fn http(status: reqwest::StatusCode) -> Self {
110        GitlabError::Http {
111            status,
112        }
113    }
114
115    fn graphql(message: Vec<graphql_client::Error>) -> Self {
116        GitlabError::GraphQL {
117            message,
118        }
119    }
120
121    fn no_response() -> Self {
122        GitlabError::NoResponse {}
123    }
124
125    fn data_type<T>(source: serde_json::Error) -> Self {
126        GitlabError::DataType {
127            source,
128            typename: any::type_name::<T>(),
129        }
130    }
131}
132
133type GitlabResult<T> = Result<T, GitlabError>;
134
135// Private enum that enables the parsing of the cert bytes to be
136// delayed until the client is built rather than when they're passed
137// to a builder.
138#[derive(Clone)]
139enum ClientCert {
140    None,
141    #[cfg(feature = "client_der")]
142    Der(Vec<u8>, String),
143    #[cfg(feature = "client_pem")]
144    Pem(Vec<u8>),
145}
146
147/// A representation of the Gitlab API for a single user.
148///
149/// Separate users should use separate instances of this.
150#[derive(Clone)]
151pub struct Gitlab {
152    /// The client to use for API calls.
153    client: Client,
154    /// The base URL to use for API calls.
155    rest_url: Url,
156    /// The URL to use for GraphQL API calls.
157    graphql_url: Url,
158    /// The authentication information to use when communicating with Gitlab.
159    auth: Auth,
160}
161
162impl Debug for Gitlab {
163    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
164        f.debug_struct("Gitlab")
165            .field("rest_url", &self.rest_url)
166            .field("graphql_url", &self.graphql_url)
167            .finish()
168    }
169}
170
171/// Should a certificate be validated in TLS connections.
172/// The Insecure option is used for self-signed certificates.
173#[derive(Debug, Clone)]
174enum CertPolicy {
175    Default,
176    /// Trust all certificates (including expired certificates). This introduces significant
177    /// vulnerabilities, and should only be used as a last resort.
178    Insecure,
179    /// Trust certificates signed by the root certificate.
180    CustomRoots(CertificateBundle),
181}
182
183fn log_request_id(headers: &HeaderMap) {
184    const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
185
186    if let Some(req_id) = headers.get(REQUEST_ID_HEADER) {
187        debug!(
188            target: "gitlab",
189            "{}: {}",
190            REQUEST_ID_HEADER.as_str(),
191            req_id.to_str().unwrap_or("<INVALID>"),
192        );
193    }
194}
195
196impl Gitlab {
197    /// Create a new Gitlab API representation.
198    ///
199    /// The `token` should be a valid [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html).
200    /// Errors out if `token` is invalid.
201    pub fn new<H, T>(host: H, token: T) -> GitlabResult<Self>
202    where
203        H: AsRef<str>,
204        T: Into<String>,
205    {
206        Self::new_impl(
207            "https",
208            host.as_ref(),
209            Auth::Token(token.into()),
210            CertPolicy::Default,
211            ClientCert::None,
212            DEFAULT_USER_AGENT,
213        )
214    }
215
216    /// Create a new non-SSL Gitlab API representation.
217    ///
218    /// The `token` should be a valid [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html).
219    /// Errors out if `token` is invalid.
220    pub fn new_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
221    where
222        H: AsRef<str>,
223        T: Into<String>,
224    {
225        Self::new_impl(
226            "http",
227            host.as_ref(),
228            Auth::Token(token.into()),
229            CertPolicy::Insecure,
230            ClientCert::None,
231            DEFAULT_USER_AGENT,
232        )
233    }
234
235    /// Create a new Gitlab API representation, with a custom root certificate.
236    ///
237    /// The `token` should be a valid [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html).
238    /// Errors out if `token` is invalid.
239    pub fn new_custom_root_certs<H, T>(
240        host: H,
241        token: T,
242        root_certificate: CertificateBundle,
243    ) -> GitlabResult<Self>
244    where
245        H: AsRef<str>,
246        T: Into<String>,
247    {
248        Self::new_impl(
249            "https",
250            host.as_ref(),
251            Auth::Token(token.into()),
252            CertPolicy::CustomRoots(root_certificate),
253            ClientCert::None,
254            DEFAULT_USER_AGENT,
255        )
256    }
257
258    /// Create a new Gitlab API representation.
259    ///
260    /// The `token` should be a valid [job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html).
261    /// Errors out if `token` is invalid.
262    pub fn new_job_token<H, T>(host: H, token: T) -> GitlabResult<Self>
263    where
264        H: AsRef<str>,
265        T: Into<String>,
266    {
267        Self::new_impl(
268            "https",
269            host.as_ref(),
270            Auth::JobToken(token.into()),
271            CertPolicy::Default,
272            ClientCert::None,
273            DEFAULT_USER_AGENT,
274        )
275    }
276
277    /// Create a new non-SSL Gitlab API representation.
278    ///
279    /// The `token` should be a valid [job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html).
280    /// Errors out if `token` is invalid.
281    pub fn new_job_token_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
282    where
283        H: AsRef<str>,
284        T: Into<String>,
285    {
286        Self::new_impl(
287            "http",
288            host.as_ref(),
289            Auth::JobToken(token.into()),
290            CertPolicy::Insecure,
291            ClientCert::None,
292            DEFAULT_USER_AGENT,
293        )
294    }
295
296    /// Create a new Gitlab API representation.
297    ///
298    /// The `token` should be a valid [OAuth2 token](https://docs.gitlab.com/ee/api/oauth2.html).
299    /// Errors out if `token` is invalid.
300    pub fn with_oauth2<H, T>(host: H, token: T) -> GitlabResult<Self>
301    where
302        H: AsRef<str>,
303        T: Into<String>,
304    {
305        Self::new_impl(
306            "https",
307            host.as_ref(),
308            Auth::OAuth2(token.into()),
309            CertPolicy::Default,
310            ClientCert::None,
311            DEFAULT_USER_AGENT,
312        )
313    }
314
315    /// Create a new non-SSL Gitlab API representation.
316    ///
317    /// The `token` should be a valid [OAuth2 token](https://docs.gitlab.com/ee/api/oauth2.html).
318    /// Errors out if `token` is invalid.
319    pub fn with_oauth2_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
320    where
321        H: AsRef<str>,
322        T: Into<String>,
323    {
324        Self::new_impl(
325            "http",
326            host.as_ref(),
327            Auth::OAuth2(token.into()),
328            CertPolicy::Default,
329            ClientCert::None,
330            DEFAULT_USER_AGENT,
331        )
332    }
333
334    /// Internal method to create a new Gitlab client.
335    fn new_impl(
336        protocol: &str,
337        host: &str,
338        auth: Auth,
339        cert_validation: CertPolicy,
340        identity: ClientCert,
341        user_agent: &str,
342    ) -> GitlabResult<Self> {
343        let rest_url = Url::parse(&format!("{protocol}://{host}/api/v4/"))?;
344        let graphql_url = Url::parse(&format!("{protocol}://{host}/api/graphql"))?;
345
346        let client_builder = Client::builder().user_agent(user_agent);
347
348        let client_builder = match cert_validation {
349            CertPolicy::Insecure => client_builder.danger_accept_invalid_certs(true),
350            CertPolicy::Default => {
351                match identity {
352                    ClientCert::None => client_builder,
353                    #[cfg(feature = "client_der")]
354                    ClientCert::Der(der, password) => {
355                        let id = TlsIdentity::from_pkcs12_der(&der, &password)?;
356                        client_builder.identity(id)
357                    },
358                    #[cfg(feature = "client_pem")]
359                    ClientCert::Pem(pem) => {
360                        let id = TlsIdentity::from_pem(&pem)?;
361                        client_builder.identity(id)
362                    },
363                }
364            },
365            CertPolicy::CustomRoots(root_certs) => {
366                let mut client_builder = client_builder;
367                for cert in root_certs.certs {
368                    client_builder = client_builder.add_root_certificate(cert)
369                }
370                client_builder
371            },
372        };
373
374        let client = client_builder.build()?;
375
376        let api = Gitlab {
377            client,
378            rest_url,
379            graphql_url,
380            auth,
381        };
382
383        // Ensure the API is working.
384        api.auth.check_connection(&api)?;
385
386        Ok(api)
387    }
388
389    /// Create a new Gitlab API client builder.
390    pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
391    where
392        H: Into<String>,
393        T: Into<String>,
394    {
395        GitlabBuilder::new(host, token)
396    }
397
398    /// Send a GraphQL query.
399    pub fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
400    where
401        Q: GraphQLQuery,
402        Q::Variables: Debug,
403        for<'d> Q::ResponseData: Deserialize<'d>,
404    {
405        info!(
406            target: "gitlab",
407            "sending GraphQL query '{}' {:?}",
408            query.operation_name,
409            query.variables,
410        );
411        let req = self.client.post(self.graphql_url.clone()).json(query);
412        let rsp: Response<Q::ResponseData> = self.send(req)?;
413
414        if let Some(errs) = rsp.errors {
415            return Err(GitlabError::graphql(errs));
416        }
417        rsp.data.ok_or_else(GitlabError::no_response)
418    }
419
420    /// Refactored code which talks to Gitlab and transforms error messages properly.
421    fn send<T>(&self, req: reqwest::blocking::RequestBuilder) -> GitlabResult<T>
422    where
423        T: DeserializeOwned,
424    {
425        let auth_headers = {
426            let mut headers = HeaderMap::default();
427            self.auth.set_header(&mut headers)?;
428            headers
429        };
430        let rsp = req.headers(auth_headers).send()?;
431
432        log_request_id(rsp.headers());
433
434        let status = rsp.status();
435        if status.is_server_error() {
436            return Err(GitlabError::http(status));
437        }
438
439        serde_json::from_reader::<_, T>(rsp).map_err(GitlabError::data_type::<T>)
440    }
441
442    /// Perform a REST query with a given auth.
443    fn rest_auth(
444        &self,
445        mut request: http::request::Builder,
446        body: Vec<u8>,
447        auth: &Auth,
448    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
449        let call = || -> Result<_, RestError> {
450            auth.set_header(request.headers_mut().unwrap())?;
451            let http_request = request.body(body)?;
452            let request = http_request.try_into()?;
453            let rsp = self.client.execute(request)?;
454
455            log_request_id(rsp.headers());
456
457            let mut http_rsp = HttpResponse::builder()
458                .status(rsp.status())
459                .version(rsp.version());
460            let headers = http_rsp.headers_mut().unwrap();
461            for (key, value) in rsp.headers() {
462                headers.insert(key, value.clone());
463            }
464            Ok(http_rsp.body(rsp.bytes()?)?)
465        };
466        call().map_err(api::ApiError::client)
467    }
468}
469
470#[derive(Debug, Error)]
471#[non_exhaustive]
472pub enum RestError {
473    #[error("error setting auth header: {}", source)]
474    AuthError {
475        #[from]
476        source: AuthError,
477    },
478    #[error("communication with gitlab: {}", source)]
479    Communication {
480        #[from]
481        source: reqwest::Error,
482    },
483    #[error("`http` error: {}", source)]
484    Http {
485        #[from]
486        source: http::Error,
487    },
488}
489
490impl api::RestClient for Gitlab {
491    type Error = RestError;
492
493    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
494        debug!(target: "gitlab", "REST api call {endpoint}");
495        Ok(self.rest_url.join(endpoint)?)
496    }
497}
498
499impl api::Client for Gitlab {
500    fn rest(
501        &self,
502        request: http::request::Builder,
503        body: Vec<u8>,
504    ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
505        self.rest_auth(request, body, &self.auth)
506    }
507}
508
509pub struct GitlabBuilder {
510    protocol: &'static str,
511    host: String,
512    token: Auth,
513    cert_validation: CertPolicy,
514    identity: ClientCert,
515    user_agent: String,
516}
517
518impl GitlabBuilder {
519    /// Create a new Gitlab API client builder.
520    pub fn new<H, T>(host: H, token: T) -> Self
521    where
522        H: Into<String>,
523        T: Into<String>,
524    {
525        Self {
526            protocol: "https",
527            host: host.into(),
528            token: Auth::Token(token.into()),
529            cert_validation: CertPolicy::Default,
530            identity: ClientCert::None,
531            user_agent: DEFAULT_USER_AGENT.to_string(),
532        }
533    }
534
535    /// Create a new unauthenticated Gitlab API client builder.
536    pub fn new_unauthenticated<H>(host: H) -> Self
537    where
538        H: Into<String>,
539    {
540        Self {
541            protocol: "https",
542            host: host.into(),
543            token: Auth::None,
544            cert_validation: CertPolicy::Default,
545            identity: ClientCert::None,
546            user_agent: DEFAULT_USER_AGENT.to_string(),
547        }
548    }
549
550    /// Create a new Gitlab API client builder with job token.
551    pub fn new_with_job_token<H, T>(host: H, token: T) -> Self
552    where
553        H: Into<String>,
554        T: Into<String>,
555    {
556        Self {
557            protocol: "https",
558            host: host.into(),
559            token: Auth::JobToken(token.into()),
560            cert_validation: CertPolicy::Default,
561            identity: ClientCert::None,
562            user_agent: DEFAULT_USER_AGENT.to_string(),
563        }
564    }
565
566    /// Switch to an insecure protocol (http instead of https).
567    pub fn insecure(&mut self) -> &mut Self {
568        self.protocol = "http";
569        self
570    }
571
572    pub fn cert_insecure(&mut self) -> &mut Self {
573        self.cert_validation = CertPolicy::Insecure;
574        self
575    }
576
577    /// Switch to using an OAuth2 token instead of a personal access token
578    pub fn oauth2_token(&mut self) -> &mut Self {
579        if let Auth::Token(token) = self.token.clone() {
580            self.token = Auth::OAuth2(token);
581        }
582        self
583    }
584
585    /// Switch to using an job token instead of a personal access token
586    pub fn job_token(&mut self) -> &mut Self {
587        if let Auth::Token(token) = self.token.clone() {
588            self.token = Auth::JobToken(token);
589        }
590        self
591    }
592
593    /// [Authenticate to Gitlab](reqwest::Identity) with the provided
594    /// DER-formatted PKCS#12 archive.
595    #[cfg(any(doc, feature = "client_der"))]
596    pub fn client_identity_from_der(&mut self, der: &[u8], password: &str) -> &mut Self {
597        self.identity = ClientCert::Der(der.into(), password.into());
598        self
599    }
600
601    /// [Authenticate to Gitlab](reqwest::Identity) with the provided
602    /// PEM-encoded private key and certificate.
603    #[cfg(any(doc, feature = "client_pem"))]
604    pub fn client_identity_from_pem(&mut self, pem: &[u8]) -> &mut Self {
605        self.identity = ClientCert::Pem(pem.into());
606        self
607    }
608
609    /// HTTP user agent to use for API calls
610    pub fn user_agent<U>(&mut self, user_agent: U) -> &mut Self
611    where
612        U: Into<String>,
613    {
614        self.user_agent = user_agent.into();
615        self
616    }
617
618    pub fn build(&self) -> GitlabResult<Gitlab> {
619        Gitlab::new_impl(
620            self.protocol,
621            &self.host,
622            self.token.clone(),
623            self.cert_validation.clone(),
624            self.identity.clone(),
625            &self.user_agent,
626        )
627    }
628
629    pub async fn build_async(&self) -> GitlabResult<AsyncGitlab> {
630        AsyncGitlab::new_impl(
631            self.protocol,
632            &self.host,
633            self.token.clone(),
634            self.cert_validation.clone(),
635            self.identity.clone(),
636            &self.user_agent,
637        )
638        .await
639    }
640}
641
642/// A representation of the asynchronous Gitlab API for a single user.
643///
644/// Separate users should use separate instances of this.
645#[derive(Clone)]
646pub struct AsyncGitlab {
647    /// The client to use for API calls.
648    client: reqwest::Client,
649    /// The base URL to use for API calls.
650    instance_url: Url,
651    /// The base URL to use for REST API calls.
652    rest_url: Url,
653    /// The URL to use for GraphQL API calls.
654    graphql_url: Url,
655    /// The authentication information to use when communicating with Gitlab.
656    auth: Auth,
657}
658
659impl Debug for AsyncGitlab {
660    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
661        f.debug_struct("AsyncGitlab")
662            .field("instance_url", &self.instance_url)
663            .field("rest_url", &self.rest_url)
664            .field("graphql_url", &self.graphql_url)
665            .finish()
666    }
667}
668
669#[async_trait]
670impl api::RestClient for AsyncGitlab {
671    type Error = RestError;
672
673    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
674        debug!(target: "gitlab", "REST api call {endpoint}");
675        Ok(self.rest_url.join(endpoint)?)
676    }
677
678    fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
679        debug!(target: "gitlab", "instance api call {endpoint}");
680        Ok(self.instance_url.join(endpoint)?)
681    }
682}
683
684#[async_trait]
685impl api::AsyncClient for AsyncGitlab {
686    async fn rest_async(
687        &self,
688        request: http::request::Builder,
689        body: Vec<u8>,
690    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
691        self.rest_async_auth(request, body, &self.auth).await
692    }
693}
694
695impl AsyncGitlab {
696    /// Internal method to create a new Gitlab client.
697    async fn new_impl(
698        protocol: &str,
699        host: &str,
700        auth: Auth,
701        cert_validation: CertPolicy,
702        identity: ClientCert,
703        user_agent: &str,
704    ) -> GitlabResult<Self> {
705        let instance_url = Url::parse(&format!("{protocol}://{host}/"))?;
706        let rest_url = Url::parse(&format!("{protocol}://{host}/api/v4/"))?;
707        let graphql_url = Url::parse(&format!("{protocol}://{host}/api/graphql"))?;
708
709        let client_builder = AsyncClient::builder().user_agent(user_agent);
710
711        let client_builder = match cert_validation {
712            CertPolicy::Insecure => client_builder.danger_accept_invalid_certs(true),
713            CertPolicy::Default => {
714                match identity {
715                    ClientCert::None => client_builder,
716                    #[cfg(feature = "client_der")]
717                    ClientCert::Der(der, password) => {
718                        let id = TlsIdentity::from_pkcs12_der(&der, &password)?;
719                        client_builder.identity(id)
720                    },
721                    #[cfg(feature = "client_pem")]
722                    ClientCert::Pem(pem) => {
723                        let id = TlsIdentity::from_pem(&pem)?;
724                        client_builder.identity(id)
725                    },
726                }
727            },
728            CertPolicy::CustomRoots(root_certs) => {
729                let mut client_builder = client_builder;
730                for cert in root_certs.certs {
731                    client_builder = client_builder.add_root_certificate(cert)
732                }
733                client_builder
734            },
735        };
736
737        let client = client_builder.build()?;
738
739        let api = AsyncGitlab {
740            client,
741            instance_url,
742            rest_url,
743            graphql_url,
744            auth,
745        };
746
747        // Ensure the API is working.
748        api.auth.check_connection_async(&api).await?;
749
750        Ok(api)
751    }
752
753    /// Send a GraphQL query.
754    pub async fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
755    where
756        Q: GraphQLQuery,
757        Q::Variables: Debug,
758        for<'d> Q::ResponseData: Deserialize<'d>,
759    {
760        info!(
761            target: "gitlab",
762            "sending GraphQL query '{}' {:?}",
763            query.operation_name,
764            query.variables,
765        );
766        let req = self.client.post(self.graphql_url.clone()).json(query);
767        let rsp: Response<Q::ResponseData> = self.send(req).await?;
768
769        if let Some(errs) = rsp.errors {
770            return Err(GitlabError::graphql(errs));
771        }
772        rsp.data.ok_or_else(GitlabError::no_response)
773    }
774
775    /// Refactored code which talks to Gitlab and transforms error messages properly.
776    async fn send<T>(&self, req: reqwest::RequestBuilder) -> GitlabResult<T>
777    where
778        T: DeserializeOwned,
779    {
780        let auth_headers = {
781            let mut headers = HeaderMap::default();
782            self.auth.set_header(&mut headers)?;
783            headers
784        };
785        let rsp = req.headers(auth_headers).send().await?;
786        let status = rsp.status();
787        if status.is_server_error() {
788            return Err(GitlabError::http(status));
789        }
790
791        serde_json::from_slice::<T>(&rsp.bytes().await?).map_err(GitlabError::data_type::<T>)
792    }
793
794    /// Perform a REST query with a given auth.
795    async fn rest_async_auth(
796        &self,
797        mut request: http::request::Builder,
798        body: Vec<u8>,
799        auth: &Auth,
800    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
801        use futures_util::TryFutureExt;
802        let call = || {
803            async {
804                auth.set_header(request.headers_mut().unwrap())?;
805                let http_request = request.body(body)?;
806                let request = http_request.try_into()?;
807                let rsp = self.client.execute(request).await?;
808
809                log_request_id(rsp.headers());
810
811                let mut http_rsp = HttpResponse::builder()
812                    .status(rsp.status())
813                    .version(rsp.version());
814                let headers = http_rsp.headers_mut().unwrap();
815                for (key, value) in rsp.headers() {
816                    headers.insert(key, value.clone());
817                }
818                Ok(http_rsp.body(rsp.bytes().await?)?)
819            }
820        };
821        call().map_err(api::ApiError::client).await
822    }
823}
824
825#[derive(Clone)]
826pub struct ImpersonationClient<'a, T> {
827    auth: Auth,
828    client: &'a T,
829}
830
831impl<'a, C> ImpersonationClient<'a, C> {
832    /// Wrap an existing client using an impersonation token.
833    pub fn new<T>(client: &'a C, token: T) -> Self
834    where
835        T: Into<String>,
836    {
837        Self {
838            auth: Auth::Token(token.into()),
839            client,
840        }
841    }
842
843    /// Switch to using an OAuth2 token instead of a personal access token
844    pub fn oauth2_token(&mut self) -> &mut Self {
845        if let Auth::Token(auth) = self.auth.clone() {
846            self.auth = Auth::OAuth2(auth);
847        }
848        self
849    }
850}
851
852impl<C> api::RestClient for ImpersonationClient<'_, C>
853where
854    C: api::RestClient,
855{
856    type Error = C::Error;
857
858    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
859        self.client.rest_endpoint(endpoint)
860    }
861
862    fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
863        self.client.instance_endpoint(endpoint)
864    }
865}
866
867impl api::Client for ImpersonationClient<'_, Gitlab> {
868    fn rest(
869        &self,
870        request: http::request::Builder,
871        body: Vec<u8>,
872    ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
873        self.client.rest_auth(request, body, &self.auth)
874    }
875}
876
877#[allow(clippy::needless_lifetimes)] // `async_trait` wants the lifetime named.
878#[async_trait]
879impl<'a> api::AsyncClient for ImpersonationClient<'a, AsyncGitlab> {
880    async fn rest_async(
881        &self,
882        request: http::request::Builder,
883        body: Vec<u8>,
884    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
885        self.client.rest_async_auth(request, body, &self.auth).await
886    }
887}