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