pipa_core/engine/
profiles.rs

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