1use 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#[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#[derive(Clone)]
113pub struct Gitlab {
114 client: Client,
116 rest_url: Url,
118 graphql_url: Url,
120 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#[derive(Debug, Clone)]
136enum CertPolicy {
137 Default,
138 Insecure,
139}
140
141impl Gitlab {
142 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 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 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 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 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 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 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 api.auth.check_connection(&api)?;
299
300 Ok(api)
301 }
302
303 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 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 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 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 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 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 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 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 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 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 #[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 #[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 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#[derive(Clone)]
555pub struct AsyncGitlab {
556 client: reqwest::Client,
558 instance_url: Url,
560 rest_url: Url,
562 graphql_url: Url,
564 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 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 api.auth.check_connection_async(&api).await?;
651
652 Ok(api)
653 }
654
655 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 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 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 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 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]
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}