1use crate::engine::log_action;
4use crate::logging::AuditLogger;
5use crate::profiles::{Profile, Profiles, load_profiles};
6
7pub struct ProfileList {
9 pub profiles: Vec<String>,
10}
11
12pub struct ProfileTestResult {
14 pub exists: bool,
15 pub testable: bool,
16 pub connected: bool,
17}
18
19pub 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
36pub 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
99pub 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, "azure" => test_azure_profile_internal(profile).await,
106 "gcs" => test_gcs_profile_internal(profile).await,
107 "sftp" => false, _ => 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 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 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 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, "iat": now
224 });
225
226 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 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 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}