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