1use crate::apis::{
2 Error, applications_api, configuration, general_api, permissions_api, sharing_api,
3};
4use crate::models;
5use http::header::{HeaderMap, HeaderValue};
6use reqwest::{Client, Request, Response};
7use reqwest_middleware::{ClientBuilder, Middleware, Next, Result as MiddlewareResult};
8use std::sync::Arc;
9use tapis_core::TokenProvider;
10
11tokio::task_local! {
12 static EXTRA_HEADERS: HeaderMap;
14}
15
16pub async fn with_headers<F, T>(headers: HeaderMap, f: F) -> T
20where
21 F: std::future::Future<Output = T>,
22{
23 EXTRA_HEADERS.scope(headers, f).await
24}
25
26fn is_allowed_extra_header(name: &reqwest::header::HeaderName) -> bool {
27 let name = name.as_str();
28 name.eq_ignore_ascii_case("x-tapis-tracking-id")
29 || name.eq_ignore_ascii_case("x_tapis_tracking_id")
30 || name.eq_ignore_ascii_case("x-request-id")
31}
32
33#[derive(Debug)]
34struct LoggingMiddleware;
35
36#[derive(Debug)]
37struct HeaderInjectionMiddleware;
38
39#[async_trait::async_trait]
40impl Middleware for LoggingMiddleware {
41 async fn handle(
42 &self,
43 req: Request,
44 extensions: &mut http::Extensions,
45 next: Next<'_>,
46 ) -> MiddlewareResult<Response> {
47 let method = req.method().clone();
48 let url = req.url().clone();
49 println!("Tapis SDK request: {} {}", method, url);
50 next.run(req, extensions).await
51 }
52}
53
54#[async_trait::async_trait]
55impl Middleware for HeaderInjectionMiddleware {
56 async fn handle(
57 &self,
58 mut req: Request,
59 extensions: &mut http::Extensions,
60 next: Next<'_>,
61 ) -> MiddlewareResult<Response> {
62 let validation = EXTRA_HEADERS.try_with(|headers| {
63 for key in headers.keys() {
64 if !is_allowed_extra_header(key) {
65 return Err(reqwest_middleware::Error::Middleware(anyhow::anyhow!(
66 "with_headers only allows request-scoped context headers; disallowed header: {}",
67 key.as_str()
68 )));
69 }
70 }
71 for (k, v) in headers {
72 req.headers_mut().insert(k, v.clone());
73 }
74 Ok::<(), reqwest_middleware::Error>(())
75 });
76 if let Ok(result) = validation {
77 result?;
78 }
79 next.run(req, extensions).await
80 }
81}
82
83fn validate_tracking_id(tracking_id: &str) -> Result<(), String> {
84 if !tracking_id.is_ascii() {
85 return Err("X-Tapis-Tracking-ID must be an entirely ASCII string.".to_string());
86 }
87 if tracking_id.len() > 126 {
88 return Err("X-Tapis-Tracking-ID must be less than 126 characters.".to_string());
89 }
90 if tracking_id.matches('.').count() != 1 {
91 return Err("X-Tapis-Tracking-ID must contain exactly one '.' (format: <namespace>.<unique_identifier>).".to_string());
92 }
93 if tracking_id.starts_with('.') || tracking_id.ends_with('.') {
94 return Err("X-Tapis-Tracking-ID cannot start or end with '.'.".to_string());
95 }
96 let (namespace, unique_id) = tracking_id.split_once('.').unwrap();
97 if !namespace.chars().all(|c| c.is_alphanumeric() || c == '_') {
98 return Err("X-Tapis-Tracking-ID namespace must contain only alphanumeric characters and underscores.".to_string());
99 }
100 if !unique_id.chars().all(|c| c.is_alphanumeric() || c == '-') {
101 return Err("X-Tapis-Tracking-ID unique identifier must contain only alphanumeric characters and hyphens.".to_string());
102 }
103 Ok(())
104}
105
106#[derive(Debug)]
107struct TrackingIdMiddleware;
108
109#[async_trait::async_trait]
110impl Middleware for TrackingIdMiddleware {
111 async fn handle(
112 &self,
113 mut req: Request,
114 extensions: &mut http::Extensions,
115 next: Next<'_>,
116 ) -> MiddlewareResult<Response> {
117 let tracking_key = req
118 .headers()
119 .keys()
120 .find(|k| {
121 let s = k.as_str();
122 s.eq_ignore_ascii_case("x-tapis-tracking-id")
123 || s.eq_ignore_ascii_case("x_tapis_tracking_id")
124 })
125 .cloned();
126 if let Some(key) = tracking_key {
127 let tracking_id = req
128 .headers()
129 .get(&key)
130 .and_then(|v| v.to_str().ok())
131 .map(|s| s.to_owned());
132 if let Some(id) = tracking_id {
133 req.headers_mut().remove(&key);
134 validate_tracking_id(&id)
135 .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!(e)))?;
136 let name = reqwest::header::HeaderName::from_static("x-tapis-tracking-id");
137 let value = reqwest::header::HeaderValue::from_str(&id)
138 .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!(e)))?;
139 req.headers_mut().insert(name, value);
140 }
141 }
142 next.run(req, extensions).await
143 }
144}
145
146fn decode_base64url(s: &str) -> Option<Vec<u8>> {
148 fn val(c: u8) -> Option<u8> {
149 match c {
150 b'A'..=b'Z' => Some(c - b'A'),
151 b'a'..=b'z' => Some(c - b'a' + 26),
152 b'0'..=b'9' => Some(c - b'0' + 52),
153 b'-' | b'+' => Some(62),
154 b'_' | b'/' => Some(63),
155 _ => None,
156 }
157 }
158 let chars: Vec<u8> = s.bytes().filter(|&b| b != b'=').collect();
159 let mut out = Vec::with_capacity(chars.len() * 3 / 4 + 1);
160 let mut i = 0;
161 while i < chars.len() {
162 let a = val(chars[i])?;
163 let b = val(*chars.get(i + 1)?)?;
164 out.push((a << 2) | (b >> 4));
165 if let Some(&c3) = chars.get(i + 2) {
166 let c = val(c3)?;
167 out.push(((b & 0x0f) << 4) | (c >> 2));
168 if let Some(&c4) = chars.get(i + 3) {
169 let d = val(c4)?;
170 out.push(((c & 0x03) << 6) | d);
171 }
172 }
173 i += 4;
174 }
175 Some(out)
176}
177
178fn extract_jwt_exp(token: &str) -> Option<i64> {
180 let payload_b64 = token.split('.').nth(1)?;
181 let bytes = decode_base64url(payload_b64)?;
182 let claims: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
183 claims.get("exp")?.as_i64()
184}
185
186struct RefreshMiddleware {
187 token_provider: Arc<dyn TokenProvider>,
188}
189
190#[async_trait::async_trait]
191impl Middleware for RefreshMiddleware {
192 async fn handle(
193 &self,
194 mut req: Request,
195 extensions: &mut http::Extensions,
196 next: Next<'_>,
197 ) -> MiddlewareResult<Response> {
198 let is_token_endpoint = {
199 let url = req.url().as_str();
200 url.contains("/oauth2/tokens") || url.contains("/v3/tokens")
201 };
202 if !is_token_endpoint {
203 let needs_refresh = req
204 .headers()
205 .get("x-tapis-token")
206 .and_then(|v| v.to_str().ok())
207 .and_then(extract_jwt_exp)
208 .map(|exp| {
209 let now = std::time::SystemTime::now()
210 .duration_since(std::time::UNIX_EPOCH)
211 .map(|d| d.as_secs() as i64)
212 .unwrap_or(0);
213 exp - now < 5
214 })
215 .unwrap_or(false);
216 if needs_refresh && let Some(new_token) = self.token_provider.get_token().await {
217 let value = HeaderValue::from_str(&new_token)
218 .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!(e)))?;
219 req.headers_mut().insert("x-tapis-token", value);
220 }
221 }
222 next.run(req, extensions).await
223 }
224}
225
226#[derive(Clone)]
227pub struct TapisApps {
228 config: Arc<configuration::Configuration>,
229 pub applications: ApplicationsClient,
230 pub general: GeneralClient,
231 pub permissions: PermissionsClient,
232 pub sharing: SharingClient,
233}
234
235impl TapisApps {
236 pub fn new(
237 base_url: &str,
238 jwt_token: Option<&str>,
239 ) -> Result<Self, Box<dyn std::error::Error>> {
240 Self::build(base_url, jwt_token, None)
241 }
242
243 pub fn with_token_provider(
247 base_url: &str,
248 jwt_token: Option<&str>,
249 provider: Arc<dyn TokenProvider>,
250 ) -> Result<Self, Box<dyn std::error::Error>> {
251 Self::build(base_url, jwt_token, Some(provider))
252 }
253
254 fn build(
255 base_url: &str,
256 jwt_token: Option<&str>,
257 token_provider: Option<Arc<dyn TokenProvider>>,
258 ) -> Result<Self, Box<dyn std::error::Error>> {
259 let mut headers = HeaderMap::new();
260 if let Some(token) = jwt_token {
261 headers.insert("X-Tapis-Token", HeaderValue::from_str(token)?);
262 }
263
264 let reqwest_client = Client::builder().default_headers(headers).build()?;
265
266 let mut builder = ClientBuilder::new(reqwest_client)
267 .with(LoggingMiddleware)
268 .with(HeaderInjectionMiddleware)
269 .with(TrackingIdMiddleware);
270
271 if let Some(provider) = token_provider {
272 builder = builder.with(RefreshMiddleware {
273 token_provider: provider,
274 });
275 }
276
277 let client = builder.build();
278
279 let config = Arc::new(configuration::Configuration {
280 base_path: base_url.to_string(),
281 client,
282 ..Default::default()
283 });
284
285 Ok(Self {
286 config: config.clone(),
287 applications: ApplicationsClient {
288 config: config.clone(),
289 },
290 general: GeneralClient {
291 config: config.clone(),
292 },
293 permissions: PermissionsClient {
294 config: config.clone(),
295 },
296 sharing: SharingClient {
297 config: config.clone(),
298 },
299 })
300 }
301
302 pub fn config(&self) -> &configuration::Configuration {
303 &self.config
304 }
305}
306
307#[derive(Clone)]
308pub struct ApplicationsClient {
309 config: Arc<configuration::Configuration>,
310}
311
312impl ApplicationsClient {
313 pub async fn change_app_owner(
314 &self,
315 app_id: &str,
316 user_name: &str,
317 ) -> Result<models::RespChangeCount, Error<applications_api::ChangeAppOwnerError>> {
318 applications_api::change_app_owner(&self.config, app_id, user_name).await
319 }
320
321 pub async fn create_app_version(
322 &self,
323 req_post_app: models::ReqPostApp,
324 ) -> Result<models::RespResourceUrl, Error<applications_api::CreateAppVersionError>> {
325 applications_api::create_app_version(&self.config, req_post_app).await
326 }
327
328 pub async fn delete_app(
329 &self,
330 app_id: &str,
331 ) -> Result<models::RespChangeCount, Error<applications_api::DeleteAppError>> {
332 applications_api::delete_app(&self.config, app_id).await
333 }
334
335 pub async fn disable_app(
336 &self,
337 app_id: &str,
338 ) -> Result<models::RespChangeCount, Error<applications_api::DisableAppError>> {
339 applications_api::disable_app(&self.config, app_id).await
340 }
341
342 pub async fn disable_app_version(
343 &self,
344 app_id: &str,
345 app_version: &str,
346 ) -> Result<models::RespChangeCount, Error<applications_api::DisableAppVersionError>> {
347 applications_api::disable_app_version(&self.config, app_id, app_version).await
348 }
349
350 pub async fn enable_app(
351 &self,
352 app_id: &str,
353 ) -> Result<models::RespChangeCount, Error<applications_api::EnableAppError>> {
354 applications_api::enable_app(&self.config, app_id).await
355 }
356
357 pub async fn enable_app_version(
358 &self,
359 app_id: &str,
360 app_version: &str,
361 ) -> Result<models::RespChangeCount, Error<applications_api::EnableAppVersionError>> {
362 applications_api::enable_app_version(&self.config, app_id, app_version).await
363 }
364
365 pub async fn get_app(
366 &self,
367 app_id: &str,
368 app_version: &str,
369 require_exec_perm: Option<bool>,
370 impersonation_id: Option<&str>,
371 select: Option<&str>,
372 resource_tenant: Option<&str>,
373 ) -> Result<models::RespApp, Error<applications_api::GetAppError>> {
374 applications_api::get_app(
375 &self.config,
376 app_id,
377 app_version,
378 require_exec_perm,
379 impersonation_id,
380 select,
381 resource_tenant,
382 )
383 .await
384 }
385
386 pub async fn get_app_latest_version(
387 &self,
388 app_id: &str,
389 require_exec_perm: Option<bool>,
390 select: Option<&str>,
391 resource_tenant: Option<&str>,
392 impersonation_id: Option<&str>,
393 ) -> Result<models::RespApp, Error<applications_api::GetAppLatestVersionError>> {
394 applications_api::get_app_latest_version(
395 &self.config,
396 app_id,
397 require_exec_perm,
398 select,
399 resource_tenant,
400 impersonation_id,
401 )
402 .await
403 }
404
405 pub async fn get_apps(
406 &self,
407 search: Option<&str>,
408 list_type: Option<models::ListTypeEnum>,
409 limit: Option<i32>,
410 order_by: Option<&str>,
411 skip: Option<i32>,
412 start_after: Option<&str>,
413 compute_total: Option<bool>,
414 select: Option<&str>,
415 show_deleted: Option<bool>,
416 impersonation_id: Option<&str>,
417 ) -> Result<models::RespApps, Error<applications_api::GetAppsError>> {
418 applications_api::get_apps(
419 &self.config,
420 search,
421 list_type,
422 limit,
423 order_by,
424 skip,
425 start_after,
426 compute_total,
427 select,
428 show_deleted,
429 impersonation_id,
430 )
431 .await
432 }
433
434 pub async fn get_history(
435 &self,
436 app_id: &str,
437 ) -> Result<models::RespAppHistory, Error<applications_api::GetHistoryError>> {
438 applications_api::get_history(&self.config, app_id).await
439 }
440
441 pub async fn is_enabled(
442 &self,
443 app_id: &str,
444 version: Option<&str>,
445 ) -> Result<models::RespBoolean, Error<applications_api::IsEnabledError>> {
446 applications_api::is_enabled(&self.config, app_id, version).await
447 }
448
449 pub async fn lock_app(
450 &self,
451 app_id: &str,
452 app_version: &str,
453 ) -> Result<models::RespChangeCount, Error<applications_api::LockAppError>> {
454 applications_api::lock_app(&self.config, app_id, app_version).await
455 }
456
457 pub async fn patch_app(
458 &self,
459 app_id: &str,
460 app_version: &str,
461 req_patch_app: models::ReqPatchApp,
462 ) -> Result<models::RespResourceUrl, Error<applications_api::PatchAppError>> {
463 applications_api::patch_app(&self.config, app_id, app_version, req_patch_app).await
464 }
465
466 pub async fn put_app(
467 &self,
468 app_id: &str,
469 app_version: &str,
470 req_put_app: models::ReqPutApp,
471 ) -> Result<models::RespResourceUrl, Error<applications_api::PutAppError>> {
472 applications_api::put_app(&self.config, app_id, app_version, req_put_app).await
473 }
474
475 pub async fn search_apps_query_parameters(
476 &self,
477 list_type: Option<models::ListTypeEnum>,
478 limit: Option<i32>,
479 order_by: Option<&str>,
480 skip: Option<i32>,
481 start_after: Option<&str>,
482 compute_total: Option<bool>,
483 select: Option<&str>,
484 ) -> Result<models::RespApps, Error<applications_api::SearchAppsQueryParametersError>> {
485 applications_api::search_apps_query_parameters(
486 &self.config,
487 list_type,
488 limit,
489 order_by,
490 skip,
491 start_after,
492 compute_total,
493 select,
494 )
495 .await
496 }
497
498 pub async fn search_apps_request_body(
499 &self,
500 req_search_apps: models::ReqSearchApps,
501 list_type: Option<models::ListTypeEnum>,
502 limit: Option<i32>,
503 order_by: Option<&str>,
504 skip: Option<i32>,
505 start_after: Option<&str>,
506 compute_total: Option<bool>,
507 select: Option<&str>,
508 ) -> Result<models::RespApps, Error<applications_api::SearchAppsRequestBodyError>> {
509 applications_api::search_apps_request_body(
510 &self.config,
511 req_search_apps,
512 list_type,
513 limit,
514 order_by,
515 skip,
516 start_after,
517 compute_total,
518 select,
519 )
520 .await
521 }
522
523 pub async fn undelete_app(
524 &self,
525 app_id: &str,
526 ) -> Result<models::RespChangeCount, Error<applications_api::UndeleteAppError>> {
527 applications_api::undelete_app(&self.config, app_id).await
528 }
529
530 pub async fn unlock_app(
531 &self,
532 app_id: &str,
533 app_version: &str,
534 ) -> Result<models::RespChangeCount, Error<applications_api::UnlockAppError>> {
535 applications_api::unlock_app(&self.config, app_id, app_version).await
536 }
537}
538
539#[derive(Clone)]
540pub struct GeneralClient {
541 config: Arc<configuration::Configuration>,
542}
543
544impl GeneralClient {
545 pub async fn health_check(
546 &self,
547 ) -> Result<models::RespBasic, Error<general_api::HealthCheckError>> {
548 general_api::health_check(&self.config).await
549 }
550
551 pub async fn ready_check(
552 &self,
553 ) -> Result<models::RespBasic, Error<general_api::ReadyCheckError>> {
554 general_api::ready_check(&self.config).await
555 }
556}
557
558#[derive(Clone)]
559pub struct PermissionsClient {
560 config: Arc<configuration::Configuration>,
561}
562
563impl PermissionsClient {
564 pub async fn get_user_perms(
565 &self,
566 app_id: &str,
567 user_name: &str,
568 ) -> Result<models::RespNameArray, Error<permissions_api::GetUserPermsError>> {
569 permissions_api::get_user_perms(&self.config, app_id, user_name).await
570 }
571
572 pub async fn grant_user_perms(
573 &self,
574 app_id: &str,
575 user_name: &str,
576 req_perms: models::ReqPerms,
577 ) -> Result<models::RespBasic, Error<permissions_api::GrantUserPermsError>> {
578 permissions_api::grant_user_perms(&self.config, app_id, user_name, req_perms).await
579 }
580
581 pub async fn revoke_user_perm(
582 &self,
583 app_id: &str,
584 user_name: &str,
585 permission: &str,
586 ) -> Result<models::RespBasic, Error<permissions_api::RevokeUserPermError>> {
587 permissions_api::revoke_user_perm(&self.config, app_id, user_name, permission).await
588 }
589
590 pub async fn revoke_user_perms(
591 &self,
592 app_id: &str,
593 user_name: &str,
594 req_perms: models::ReqPerms,
595 ) -> Result<models::RespBasic, Error<permissions_api::RevokeUserPermsError>> {
596 permissions_api::revoke_user_perms(&self.config, app_id, user_name, req_perms).await
597 }
598}
599
600#[derive(Clone)]
601pub struct SharingClient {
602 config: Arc<configuration::Configuration>,
603}
604
605impl SharingClient {
606 pub async fn get_share_info(
607 &self,
608 app_id: &str,
609 ) -> Result<models::RespShareInfo, Error<sharing_api::GetShareInfoError>> {
610 sharing_api::get_share_info(&self.config, app_id).await
611 }
612
613 pub async fn share_app(
614 &self,
615 app_id: &str,
616 req_share_update: models::ReqShareUpdate,
617 ) -> Result<models::RespBasic, Error<sharing_api::ShareAppError>> {
618 sharing_api::share_app(&self.config, app_id, req_share_update).await
619 }
620
621 pub async fn share_app_public(
622 &self,
623 app_id: &str,
624 ) -> Result<models::RespBasic, Error<sharing_api::ShareAppPublicError>> {
625 sharing_api::share_app_public(&self.config, app_id).await
626 }
627
628 pub async fn un_share_app(
629 &self,
630 app_id: &str,
631 req_share_update: models::ReqShareUpdate,
632 ) -> Result<models::RespBasic, Error<sharing_api::UnShareAppError>> {
633 sharing_api::un_share_app(&self.config, app_id, req_share_update).await
634 }
635
636 pub async fn un_share_app_public(
637 &self,
638 app_id: &str,
639 ) -> Result<models::RespBasic, Error<sharing_api::UnShareAppPublicError>> {
640 sharing_api::un_share_app_public(&self.config, app_id).await
641 }
642}