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