Skip to main content

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.tls_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) => client_builder.tls_certs_merge(root_certs.certs),
366        };
367
368        let client = client_builder.build()?;
369
370        let api = Gitlab {
371            client,
372            rest_url,
373            graphql_url,
374            auth,
375        };
376
377        // Ensure the API is working.
378        api.auth.check_connection(&api)?;
379
380        Ok(api)
381    }
382
383    /// Create a new Gitlab API client builder.
384    pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
385    where
386        H: Into<String>,
387        T: Into<String>,
388    {
389        GitlabBuilder::new(host, token)
390    }
391
392    /// Send a GraphQL query.
393    pub fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
394    where
395        Q: GraphQLQuery,
396        Q::Variables: Debug,
397        for<'d> Q::ResponseData: Deserialize<'d>,
398    {
399        info!(
400            target: "gitlab",
401            "sending GraphQL query '{}' {:?}",
402            query.operation_name,
403            query.variables,
404        );
405        let req = self.client.post(self.graphql_url.clone()).json(query);
406        let rsp: Response<Q::ResponseData> = self.send(req)?;
407
408        if let Some(errs) = rsp.errors {
409            return Err(GitlabError::graphql(errs));
410        }
411        rsp.data.ok_or_else(GitlabError::no_response)
412    }
413
414    /// Refactored code which talks to Gitlab and transforms error messages properly.
415    fn send<T>(&self, req: reqwest::blocking::RequestBuilder) -> GitlabResult<T>
416    where
417        T: DeserializeOwned,
418    {
419        let auth_headers = {
420            let mut headers = HeaderMap::default();
421            self.auth.set_header(&mut headers)?;
422            headers
423        };
424        let rsp = req.headers(auth_headers).send()?;
425
426        log_request_id(rsp.headers());
427
428        let status = rsp.status();
429        if status.is_server_error() {
430            return Err(GitlabError::http(status));
431        }
432
433        serde_json::from_reader::<_, T>(rsp).map_err(GitlabError::data_type::<T>)
434    }
435
436    /// Perform a REST query with a given auth.
437    fn rest_auth(
438        &self,
439        mut request: http::request::Builder,
440        body: Vec<u8>,
441        auth: &Auth,
442    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
443        let call = || -> Result<_, RestError> {
444            auth.set_header(request.headers_mut().unwrap())?;
445            let http_request = request.body(body)?;
446            let request = http_request.try_into()?;
447            let rsp = self.client.execute(request)?;
448
449            log_request_id(rsp.headers());
450
451            let mut http_rsp = HttpResponse::builder()
452                .status(rsp.status())
453                .version(rsp.version());
454            let headers = http_rsp.headers_mut().unwrap();
455            for (key, value) in rsp.headers() {
456                headers.insert(key, value.clone());
457            }
458            Ok(http_rsp.body(rsp.bytes()?)?)
459        };
460        call().map_err(api::ApiError::client)
461    }
462}
463
464#[derive(Debug, Error)]
465#[non_exhaustive]
466pub enum RestError {
467    #[error("error setting auth header: {}", source)]
468    AuthError {
469        #[from]
470        source: AuthError,
471    },
472    #[error("communication with gitlab: {}", source)]
473    Communication {
474        #[from]
475        source: reqwest::Error,
476    },
477    #[error("`http` error: {}", source)]
478    Http {
479        #[from]
480        source: http::Error,
481    },
482}
483
484impl api::RestClient for Gitlab {
485    type Error = RestError;
486
487    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
488        debug!(target: "gitlab", "REST api call {endpoint}");
489        Ok(self.rest_url.join(endpoint)?)
490    }
491}
492
493impl api::Client for Gitlab {
494    fn rest(
495        &self,
496        request: http::request::Builder,
497        body: Vec<u8>,
498    ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
499        self.rest_auth(request, body, &self.auth)
500    }
501}
502
503pub struct GitlabBuilder {
504    protocol: &'static str,
505    host: String,
506    token: Auth,
507    cert_validation: CertPolicy,
508    identity: ClientCert,
509    user_agent: String,
510}
511
512impl GitlabBuilder {
513    /// Create a new Gitlab API client builder.
514    pub fn new<H, T>(host: H, token: T) -> Self
515    where
516        H: Into<String>,
517        T: Into<String>,
518    {
519        Self {
520            protocol: "https",
521            host: host.into(),
522            token: Auth::Token(token.into()),
523            cert_validation: CertPolicy::Default,
524            identity: ClientCert::None,
525            user_agent: DEFAULT_USER_AGENT.to_string(),
526        }
527    }
528
529    /// Create a new unauthenticated Gitlab API client builder.
530    pub fn new_unauthenticated<H>(host: H) -> Self
531    where
532        H: Into<String>,
533    {
534        Self {
535            protocol: "https",
536            host: host.into(),
537            token: Auth::None,
538            cert_validation: CertPolicy::Default,
539            identity: ClientCert::None,
540            user_agent: DEFAULT_USER_AGENT.to_string(),
541        }
542    }
543
544    /// Create a new Gitlab API client builder with job token.
545    pub fn new_with_job_token<H, T>(host: H, token: T) -> Self
546    where
547        H: Into<String>,
548        T: Into<String>,
549    {
550        Self {
551            protocol: "https",
552            host: host.into(),
553            token: Auth::JobToken(token.into()),
554            cert_validation: CertPolicy::Default,
555            identity: ClientCert::None,
556            user_agent: DEFAULT_USER_AGENT.to_string(),
557        }
558    }
559
560    /// Switch to an insecure protocol (http instead of https).
561    pub fn insecure(&mut self) -> &mut Self {
562        self.protocol = "http";
563        self
564    }
565
566    pub fn cert_insecure(&mut self) -> &mut Self {
567        self.cert_validation = CertPolicy::Insecure;
568        self
569    }
570
571    /// Switch to using an OAuth2 token instead of a personal access token
572    pub fn oauth2_token(&mut self) -> &mut Self {
573        if let Auth::Token(token) = self.token.clone() {
574            self.token = Auth::OAuth2(token);
575        }
576        self
577    }
578
579    /// Switch to using an job token instead of a personal access token
580    pub fn job_token(&mut self) -> &mut Self {
581        if let Auth::Token(token) = self.token.clone() {
582            self.token = Auth::JobToken(token);
583        }
584        self
585    }
586
587    /// [Authenticate to Gitlab](reqwest::Identity) with the provided
588    /// DER-formatted PKCS#12 archive.
589    #[cfg(any(doc, feature = "client_der"))]
590    pub fn client_identity_from_der(&mut self, der: &[u8], password: &str) -> &mut Self {
591        self.identity = ClientCert::Der(der.into(), password.into());
592        self
593    }
594
595    /// [Authenticate to Gitlab](reqwest::Identity) with the provided
596    /// PEM-encoded private key and certificate.
597    #[cfg(any(doc, feature = "client_pem"))]
598    pub fn client_identity_from_pem(&mut self, pem: &[u8]) -> &mut Self {
599        self.identity = ClientCert::Pem(pem.into());
600        self
601    }
602
603    /// HTTP user agent to use for API calls
604    pub fn user_agent<U>(&mut self, user_agent: U) -> &mut Self
605    where
606        U: Into<String>,
607    {
608        self.user_agent = user_agent.into();
609        self
610    }
611
612    pub fn build(&self) -> GitlabResult<Gitlab> {
613        Gitlab::new_impl(
614            self.protocol,
615            &self.host,
616            self.token.clone(),
617            self.cert_validation.clone(),
618            self.identity.clone(),
619            &self.user_agent,
620        )
621    }
622
623    pub async fn build_async(&self) -> GitlabResult<AsyncGitlab> {
624        AsyncGitlab::new_impl(
625            self.protocol,
626            &self.host,
627            self.token.clone(),
628            self.cert_validation.clone(),
629            self.identity.clone(),
630            &self.user_agent,
631        )
632        .await
633    }
634}
635
636/// A representation of the asynchronous Gitlab API for a single user.
637///
638/// Separate users should use separate instances of this.
639#[derive(Clone)]
640pub struct AsyncGitlab {
641    /// The client to use for API calls.
642    client: reqwest::Client,
643    /// The base URL to use for API calls.
644    instance_url: Url,
645    /// The base URL to use for REST API calls.
646    rest_url: Url,
647    /// The URL to use for GraphQL API calls.
648    graphql_url: Url,
649    /// The authentication information to use when communicating with Gitlab.
650    auth: Auth,
651}
652
653impl Debug for AsyncGitlab {
654    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
655        f.debug_struct("AsyncGitlab")
656            .field("instance_url", &self.instance_url)
657            .field("rest_url", &self.rest_url)
658            .field("graphql_url", &self.graphql_url)
659            .finish()
660    }
661}
662
663#[async_trait]
664impl api::RestClient for AsyncGitlab {
665    type Error = RestError;
666
667    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
668        debug!(target: "gitlab", "REST api call {endpoint}");
669        Ok(self.rest_url.join(endpoint)?)
670    }
671
672    fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
673        debug!(target: "gitlab", "instance api call {endpoint}");
674        Ok(self.instance_url.join(endpoint)?)
675    }
676}
677
678#[async_trait]
679impl api::AsyncClient for AsyncGitlab {
680    async fn rest_async(
681        &self,
682        request: http::request::Builder,
683        body: Vec<u8>,
684    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
685        self.rest_async_auth(request, body, &self.auth).await
686    }
687}
688
689impl AsyncGitlab {
690    /// Internal method to create a new Gitlab client.
691    async fn new_impl(
692        protocol: &str,
693        host: &str,
694        auth: Auth,
695        cert_validation: CertPolicy,
696        identity: ClientCert,
697        user_agent: &str,
698    ) -> GitlabResult<Self> {
699        let instance_url = Url::parse(&format!("{protocol}://{host}/"))?;
700        let rest_url = Url::parse(&format!("{protocol}://{host}/api/v4/"))?;
701        let graphql_url = Url::parse(&format!("{protocol}://{host}/api/graphql"))?;
702
703        let client_builder = AsyncClient::builder().user_agent(user_agent);
704
705        let client_builder = match cert_validation {
706            CertPolicy::Insecure => client_builder.tls_danger_accept_invalid_certs(true),
707            CertPolicy::Default => {
708                match identity {
709                    ClientCert::None => client_builder,
710                    #[cfg(feature = "client_der")]
711                    ClientCert::Der(der, password) => {
712                        let id = TlsIdentity::from_pkcs12_der(&der, &password)?;
713                        client_builder.identity(id)
714                    },
715                    #[cfg(feature = "client_pem")]
716                    ClientCert::Pem(pem) => {
717                        let id = TlsIdentity::from_pem(&pem)?;
718                        client_builder.identity(id)
719                    },
720                }
721            },
722            CertPolicy::CustomRoots(root_certs) => client_builder.tls_certs_merge(root_certs.certs),
723        };
724
725        let client = client_builder.build()?;
726
727        let api = AsyncGitlab {
728            client,
729            instance_url,
730            rest_url,
731            graphql_url,
732            auth,
733        };
734
735        // Ensure the API is working.
736        api.auth.check_connection_async(&api).await?;
737
738        Ok(api)
739    }
740
741    /// Send a GraphQL query.
742    pub async fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
743    where
744        Q: GraphQLQuery,
745        Q::Variables: Debug,
746        for<'d> Q::ResponseData: Deserialize<'d>,
747    {
748        info!(
749            target: "gitlab",
750            "sending GraphQL query '{}' {:?}",
751            query.operation_name,
752            query.variables,
753        );
754        let req = self.client.post(self.graphql_url.clone()).json(query);
755        let rsp: Response<Q::ResponseData> = self.send(req).await?;
756
757        if let Some(errs) = rsp.errors {
758            return Err(GitlabError::graphql(errs));
759        }
760        rsp.data.ok_or_else(GitlabError::no_response)
761    }
762
763    /// Refactored code which talks to Gitlab and transforms error messages properly.
764    async fn send<T>(&self, req: reqwest::RequestBuilder) -> GitlabResult<T>
765    where
766        T: DeserializeOwned,
767    {
768        let auth_headers = {
769            let mut headers = HeaderMap::default();
770            self.auth.set_header(&mut headers)?;
771            headers
772        };
773        let rsp = req.headers(auth_headers).send().await?;
774        let status = rsp.status();
775        if status.is_server_error() {
776            return Err(GitlabError::http(status));
777        }
778
779        serde_json::from_slice::<T>(&rsp.bytes().await?).map_err(GitlabError::data_type::<T>)
780    }
781
782    /// Perform a REST query with a given auth.
783    async fn rest_async_auth(
784        &self,
785        mut request: http::request::Builder,
786        body: Vec<u8>,
787        auth: &Auth,
788    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
789        use futures_util::TryFutureExt;
790        let call = || {
791            async {
792                auth.set_header(request.headers_mut().unwrap())?;
793                let http_request = request.body(body)?;
794                let request = http_request.try_into()?;
795                let rsp = self.client.execute(request).await?;
796
797                log_request_id(rsp.headers());
798
799                let mut http_rsp = HttpResponse::builder()
800                    .status(rsp.status())
801                    .version(rsp.version());
802                let headers = http_rsp.headers_mut().unwrap();
803                for (key, value) in rsp.headers() {
804                    headers.insert(key, value.clone());
805                }
806                Ok(http_rsp.body(rsp.bytes().await?)?)
807            }
808        };
809        call().map_err(api::ApiError::client).await
810    }
811}
812
813#[derive(Clone)]
814pub struct ImpersonationClient<'a, T> {
815    auth: Auth,
816    client: &'a T,
817}
818
819impl<'a, C> ImpersonationClient<'a, C> {
820    /// Wrap an existing client using an impersonation token.
821    pub fn new<T>(client: &'a C, token: T) -> Self
822    where
823        T: Into<String>,
824    {
825        Self {
826            auth: Auth::Token(token.into()),
827            client,
828        }
829    }
830
831    /// Switch to using an OAuth2 token instead of a personal access token
832    pub fn oauth2_token(&mut self) -> &mut Self {
833        if let Auth::Token(auth) = self.auth.clone() {
834            self.auth = Auth::OAuth2(auth);
835        }
836        self
837    }
838}
839
840impl<C> api::RestClient for ImpersonationClient<'_, C>
841where
842    C: api::RestClient,
843{
844    type Error = C::Error;
845
846    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
847        self.client.rest_endpoint(endpoint)
848    }
849
850    fn instance_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
851        self.client.instance_endpoint(endpoint)
852    }
853}
854
855impl api::Client for ImpersonationClient<'_, Gitlab> {
856    fn rest(
857        &self,
858        request: http::request::Builder,
859        body: Vec<u8>,
860    ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
861        self.client.rest_auth(request, body, &self.auth)
862    }
863}
864
865#[allow(clippy::needless_lifetimes)] // `async_trait` wants the lifetime named.
866#[async_trait]
867impl<'a> api::AsyncClient for ImpersonationClient<'a, AsyncGitlab> {
868    async fn rest_async(
869        &self,
870        request: http::request::Builder,
871        body: Vec<u8>,
872    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
873        self.client.rest_async_auth(request, body, &self.auth).await
874    }
875}