pipa_core/engine/
profiles.rs

1//! Profile management functions for the engine
2
3use crate::engine::log_action;
4use crate::profiles::{Profile, Profiles, load_profiles};
5
6/// Result of listing profiles
7pub struct ProfileList {
8    pub profiles: Vec<String>,
9}
10
11/// Result of testing a profile
12pub struct ProfileTestResult {
13    pub exists: bool,
14    pub testable: bool,
15    pub connected: bool,
16}
17
18/// List all available profiles
19pub fn list_profiles() -> Result<(ProfileList, String), String> {
20    match load_profiles() {
21        Ok(profiles) => {
22            let profile_names: Vec<String> = profiles.keys().cloned().collect();
23            let message = log_action("profiles_listed", None, None, None, None);
24            Ok((
25                ProfileList {
26                    profiles: profile_names,
27                },
28                message,
29            ))
30        }
31        Err(_) => Err("Failed to load profiles".to_string()),
32    }
33}
34
35/// Test a profile's connectivity
36pub async fn test_profile(profile_name: &str) -> (ProfileTestResult, String) {
37    let profiles = match load_profiles() {
38        Ok(p) => p,
39        Err(_) => {
40            let message = log_action(
41                "profile_tested",
42                Some("exists=false, testable=false, connected=false"),
43                None,
44                None,
45                Some(profile_name),
46            );
47            return (
48                ProfileTestResult {
49                    exists: false,
50                    testable: false,
51                    connected: false,
52                },
53                message,
54            );
55        }
56    };
57
58    if let Some(_profile) = profiles.get(profile_name) {
59        let connected = test_profile_internal(profile_name, &profiles).await;
60        let details = format!("exists=true, testable=true, connected={}", connected);
61        let message = log_action(
62            "profile_tested",
63            Some(&details),
64            None,
65            None,
66            Some(profile_name),
67        );
68        (
69            ProfileTestResult {
70                exists: true,
71                testable: true,
72                connected,
73            },
74            message,
75        )
76    } else {
77        let message = log_action(
78            "profile_tested",
79            Some("exists=false, testable=false, connected=false"),
80            None,
81            None,
82            Some(profile_name),
83        );
84        (
85            ProfileTestResult {
86                exists: false,
87                testable: false,
88                connected: false,
89            },
90            message,
91        )
92    }
93}
94
95// Extracted for reuse - test profile connectivity
96pub async fn test_profile_internal(profile_name: &str, profiles: &Profiles) -> bool {
97    if let Some(profile) = profiles.get(profile_name) {
98        match profile.provider.as_str() {
99            "s3" => test_s3_profile_internal(profile).await,
100            "local" => true, // Local always works if profile exists
101            "azure" => test_azure_profile_internal(profile).await,
102            "gcs" => test_gcs_profile_internal(profile).await,
103            "sftp" => false, // Not implemented yet
104            _ => false,
105        }
106    } else {
107        false
108    }
109}
110
111async fn test_s3_profile_internal(profile: &Profile) -> bool {
112    use aws_sdk_s3::config::Credentials;
113
114    let region = profile
115        .region
116        .clone()
117        .unwrap_or_else(|| "us-east-1".to_string());
118    let mut cfg_loader = aws_config::defaults(aws_config::BehaviorVersion::latest())
119        .region(aws_config::Region::new(region));
120
121    if let Some(endpoint) = &profile.endpoint {
122        cfg_loader = cfg_loader.endpoint_url(endpoint);
123    }
124
125    let base = cfg_loader.load().await;
126    let mut s3b = aws_sdk_s3::config::Builder::from(&base);
127
128    if profile.path_style.unwrap_or(false) {
129        s3b = s3b.force_path_style(true);
130    }
131
132    // Handle optional credentials
133    if let (Some(access_key), Some(secret_key)) = (&profile.access_key, &profile.secret_key) {
134        if !access_key.is_empty() && !secret_key.is_empty() {
135            let creds = Credentials::new(
136                access_key.clone(),
137                secret_key.clone(),
138                None,
139                None,
140                "profile",
141            );
142            s3b = s3b.credentials_provider(creds);
143        }
144    }
145
146    let client = aws_sdk_s3::Client::from_conf(s3b.build());
147    client.list_buckets().send().await.is_ok()
148}
149
150async fn test_azure_profile_internal(profile: &Profile) -> bool {
151    if let Some(connection_string) = &profile.connection_string {
152        !connection_string.is_empty()
153    } else {
154        false
155    }
156}
157
158async fn test_gcs_profile_internal(profile: &Profile) -> bool {
159    if let Some(service_account_json) = &profile.service_account_json {
160        return test_gcs_service_account(service_account_json).await;
161    }
162    false
163}
164
165fn parse_gcs_service_account(
166    service_account_json: &str,
167) -> Result<(String, String, String), Box<dyn std::error::Error>> {
168    use serde_json::Value;
169
170    let json: Value = serde_json::from_str(service_account_json)?;
171
172    let project_id = json["project_id"]
173        .as_str()
174        .ok_or("Missing project_id in service account JSON")?
175        .to_string();
176
177    let client_email = json["client_email"]
178        .as_str()
179        .ok_or("Missing client_email in service account JSON")?
180        .to_string();
181
182    let private_key = json["private_key"]
183        .as_str()
184        .ok_or("Missing private_key in service account JSON")?
185        .to_string();
186
187    Ok((project_id, client_email, private_key))
188}
189
190async fn test_gcs_service_account(service_account_json: &str) -> bool {
191    println!(
192        "Debug: GCS service account JSON length: {}",
193        service_account_json.len()
194    );
195    // Don't log sensitive service account details
196    // println!(
197    //     "Debug: GCS service account JSON first 100 chars: {}",
198    //     &service_account_json.chars().take(100).collect::<String>()
199    // );
200    use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
201    use serde_json::json;
202
203    let (project_id, client_email, private_key) =
204        match parse_gcs_service_account(service_account_json) {
205            Ok((pid, email, key)) => (pid, email, key),
206            Err(e) => {
207                println!("Debug: GCS service account parsing failed: {}", e);
208                return false;
209            }
210        };
211
212    // Create JWT claims
213    let now = chrono::Utc::now().timestamp();
214    let claims = json!({
215        "iss": client_email,
216        "scope": "https://www.googleapis.com/auth/cloud-platform",
217        "aud": "https://oauth2.googleapis.com/token",
218        "exp": now + 3600, // 1 hour
219        "iat": now
220    });
221
222    // Generate JWT token
223    let header = Header::new(Algorithm::RS256);
224    let encoding_key = match EncodingKey::from_rsa_pem(private_key.as_bytes()) {
225        Ok(key) => key,
226        Err(e) => {
227            println!("Debug: GCS private key parsing failed: {}", e);
228            return false;
229        }
230    };
231
232    let jwt_token = match encode(&header, &claims, &encoding_key) {
233        Ok(token) => token,
234        Err(e) => {
235            println!("Debug: GCS JWT generation failed: {}", e);
236            return false;
237        }
238    };
239
240    println!("Debug: GCS JWT generated successfully");
241
242    // Exchange JWT for access token
243    let client = reqwest::Client::new();
244
245    let token_response = match client
246        .post("https://oauth2.googleapis.com/token")
247        .header("Content-Type", "application/x-www-form-urlencoded")
248        .form(&[
249            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
250            ("assertion", &jwt_token),
251        ])
252        .send()
253        .await
254    {
255        Ok(response) => response,
256        Err(e) => {
257            println!("Debug: GCS token exchange request failed: {}", e);
258            return false;
259        }
260    };
261
262    if !token_response.status().is_success() {
263        println!(
264            "Debug: GCS token exchange failed: {}",
265            token_response.status()
266        );
267        return false;
268    }
269
270    let access_token = match token_response.json::<serde_json::Value>().await {
271        Ok(json) => match json["access_token"].as_str() {
272            Some(token) => token.to_string(),
273            None => {
274                println!("Debug: GCS access_token not found in response");
275                return false;
276            }
277        },
278        Err(e) => {
279            println!("Debug: GCS token response parsing failed: {}", e);
280            return false;
281        }
282    };
283
284    // Test bucket list API
285    let bucket_list_url = format!(
286        "https://storage.googleapis.com/storage/v1/b?project={}",
287        project_id
288    );
289
290    match client
291        .get(&bucket_list_url)
292        .header("Authorization", format!("Bearer {}", access_token))
293        .send()
294        .await
295    {
296        Ok(response) => {
297            println!("Debug: GCS bucket list response: {}", response.status());
298            response.status().is_success()
299        }
300        Err(e) => {
301            println!("Debug: GCS bucket list failed: {}", e);
302            false
303        }
304    }
305}