Skip to main content

ziti_sdk/session/
manager.rs

1//! Session lifecycle management
2//!
3//! Manages API and network sessions with automatic renewal.
4
5use super::{api_session, ApiSession, NetworkSession};
6use crate::error::{ZitiError, ZitiResult};
7use crate::identity::IdentityManager;
8use crate::transport::http::controller_client;
9use serde::Deserialize;
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::RwLock;
13
14/// Response structure for the create-session API call
15#[derive(Debug, Deserialize)]
16struct CreateSessionResponse {
17    data: SessionData,
18}
19
20#[derive(Debug, Deserialize)]
21struct SessionData {
22    id: String,
23}
24
25/// Session manager for handling session lifecycle
26#[derive(Clone)]
27pub struct SessionManager {
28    identity_manager: Arc<IdentityManager>,
29    current_api_session: Arc<RwLock<Option<ApiSession>>>,
30}
31
32impl SessionManager {
33    /// Create a new SessionManager with the provided identity manager
34    pub fn new(identity_manager: IdentityManager) -> Self {
35        Self {
36            identity_manager: Arc::new(identity_manager),
37            current_api_session: Arc::new(RwLock::new(None)),
38        }
39    }
40
41    /// Get a valid API session, authenticating if necessary
42    ///
43    /// This method checks if there's a current valid session, and if not,
44    /// it will authenticate with the controller to obtain a new one.
45    ///
46    /// # Returns
47    ///
48    /// * `ZitiResult<ApiSession>` - A valid API session
49    ///
50    /// # Example
51    ///
52    /// ```rust,no_run
53    /// use ziti_sdk::session::SessionManager;
54    /// use ziti_sdk::identity::load_from_file;
55    ///
56    /// #[tokio::main]
57    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
58    ///     let identity = load_from_file("identity.json").await?;
59    ///     let session_manager = SessionManager::new(identity);
60    ///     let api_session = session_manager.get_api_session().await?;
61    ///     println!("Got API session: {}", api_session.id);
62    ///     Ok(())
63    /// }
64    /// ```
65    pub async fn get_api_session(&self) -> ZitiResult<ApiSession> {
66        // Check if we have a current valid session
67        {
68            let session_guard = self.current_api_session.read().await;
69            if let Some(ref session) = *session_guard
70                && !session.is_expired()
71            {
72                return Ok(session.clone());
73            }
74        }
75
76        // Session is expired or doesn't exist, authenticate
77        let new_session = api_session::authenticate(&self.identity_manager).await?;
78
79        // Store the new session
80        {
81            let mut session_guard = self.current_api_session.write().await;
82            *session_guard = Some(new_session.clone());
83        }
84
85        Ok(new_session)
86    }
87
88    /// Get a network session for a specific service
89    ///
90    /// This method will obtain an API session first (if needed), then request
91    /// a network session for the specified service.
92    ///
93    /// # Arguments
94    ///
95    /// * `service_id` - The ID of the service to get a session for
96    ///
97    /// # Returns
98    ///
99    /// * `ZitiResult<NetworkSession>` - A network session for the service
100    pub async fn get_network_session(&self, service_id: &str) -> ZitiResult<NetworkSession> {
101        // Ensure we have a valid API session to authorize the request.
102        let api_session = self.get_api_session().await?;
103
104        let client = controller_client(&self.identity_manager, Duration::from_secs(30)).await?;
105
106        let sessions_url = format!(
107            "{}/sessions",
108            self.identity_manager.zt_api().trim_end_matches('/')
109        );
110
111        let request_body = serde_json::json!({
112            "serviceId": service_id,
113            "type": "Dial",
114        });
115
116        let response = client
117            .post(&sessions_url)
118            .header("Content-Type", "application/json")
119            .header("zt-session", &api_session.token)
120            .json(&request_body)
121            .send()
122            .await
123            .map_err(|e| {
124                ZitiError::ConnectionFailed(format!("Failed to create network session: {}", e))
125            })?;
126
127        if !response.status().is_success() {
128            let status = response.status();
129            let error_text = response
130                .text()
131                .await
132                .unwrap_or_else(|_| "Unknown error".to_string());
133            return Err(ZitiError::ProtocolError {
134                message: format!(
135                    "Network session request failed with status {}: {}",
136                    status, error_text
137                ),
138            });
139        }
140
141        let session_response: CreateSessionResponse =
142            response.json().await.map_err(|e| ZitiError::ProtocolError {
143                message: format!("Failed to parse network session response: {}", e),
144            })?;
145
146        Ok(NetworkSession::new(
147            session_response.data.id,
148            service_id.to_string(),
149        ))
150    }
151
152    /// Clear the current API session
153    ///
154    /// This method clears the stored API session, forcing the next call to
155    /// `get_api_session()` to authenticate again.
156    pub async fn clear_api_session(&self) {
157        let mut session_guard = self.current_api_session.write().await;
158        *session_guard = None;
159    }
160
161    /// Check if the current API session is valid
162    ///
163    /// # Returns
164    ///
165    /// * `bool` - True if there's a valid (non-expired) API session
166    pub async fn has_valid_api_session(&self) -> bool {
167        let session_guard = self.current_api_session.read().await;
168        if let Some(ref session) = *session_guard {
169            !session.is_expired()
170        } else {
171            false
172        }
173    }
174
175    /// Get the identity manager associated with this session manager
176    pub fn identity_manager(&self) -> &IdentityManager {
177        &self.identity_manager
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    // Note: comprehensive tests require a controller and proper identity setup,
184    // which is exercised by the integration tests rather than here.
185}