1use base64::{engine::general_purpose, Engine as _};
69use clap::{Parser, Subcommand};
70use eyre::{Context, Result};
71use newton_prover_core::config::NewtonAvsConfig;
72use pkcs8::DecodePublicKey;
73use rsa::{rand_core::OsRng, Oaep, RsaPublicKey};
74use serde::{Deserialize, Serialize};
75use sha2::Sha256;
76use tracing::{self, info};
77
78use crate::config::NewtonCliConfig;
79
80const KMS_PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY-----
83MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5aOjaOmoIiITeXSIe5Yt
849vmugRSahtf+MyMXEKB/kY96/UFIKCPGHXO7XF7RsccTgCYSi6JgTH2DiD1saazd
85mJaHWMnz/UDY7/omFnW6RxGmGQxxXVI2bHgUhvf50JEculJvuUiljBXTUnofrfJI
86LhZ2bAls3BBDDCxW8OFeJ3zQhJdOL9JP4AaYyiXlZZ1xpmDB3IEW+s9ma4524lqg
87j/dmSVk40IB2K1EhaPj/zKs37nH2uOnLBkE2OWzQoEAVTdd6B1TLcFiGFrR9d4Xk
889RqHifqyaIlDWjUZ1lO9GX++2x6YIq9q+jDzidOzi43QLfBKjHELmM6xIXQ+Vwqa
89WwIDAQAB
90-----END PUBLIC KEY-----"#;
91
92pub fn encrypt_data(data: &[u8], public_key_pem: &str) -> Result<String> {
104 let public_key =
106 RsaPublicKey::from_public_key_pem(public_key_pem).context("Failed to parse RSA public key from PEM")?;
107
108 let mut rng = OsRng;
110 let padding = Oaep::new::<Sha256>();
111 let ciphertext = public_key
112 .encrypt(&mut rng, padding, data)
113 .context("Failed to encrypt data with RSA-OAEP")?;
114
115 let b64 = general_purpose::STANDARD.encode(ciphertext);
117 Ok(b64)
118}
119
120#[derive(Debug, Serialize, Deserialize)]
122pub struct PolicyResponse {
123 pub id: String,
124}
125
126#[derive(Debug, Serialize, Deserialize)]
128pub struct SecretRequest {
129 pub key: String,
130 pub value: String,
131}
132
133#[derive(Debug, Serialize, Deserialize)]
135pub struct CliTokenResponse {
136 pub token: Option<String>,
137 pub status: String, }
139
140pub async fn create_policy_data(
152 api_base_url: &str,
153 policy_name: &str,
154 policy_data_address: &str,
155 bearer_token: &str,
156 verify_tls: bool,
157) -> Result<String> {
158 let url = format!("{}/v1/policy-data", api_base_url.trim_end_matches('/'));
159
160 let client = if verify_tls {
161 reqwest::Client::new()
162 } else {
163 reqwest::Client::builder()
164 .danger_accept_invalid_certs(true)
165 .build()
166 .context("Failed to create HTTP client")?
167 };
168
169 let response = client
170 .post(&url)
171 .header("Authorization", format!("Bearer {}", bearer_token))
172 .json(&serde_json::json!({
173 "name": policy_name,
174 "address": policy_data_address
175 }))
176 .send()
177 .await
178 .with_context(|| format!("Failed to send request to {}", url))?;
179
180 let status = response.status();
181 if !status.is_success() {
182 let error_text = response.text().await.unwrap_or_default();
183 return Err(eyre::eyre!("API request failed with status {}: {}", status, error_text));
184 }
185
186 let policy_response: PolicyResponse = response.json().await.context("Failed to parse policy response")?;
187
188 Ok(policy_response.id)
189}
190
191pub async fn create_policy_call(
203 api_base_url: &str,
204 policy_name: &str,
205 policy_address: &str,
206 bearer_token: &str,
207 verify_tls: bool,
208) -> Result<String> {
209 let url = format!("{}/v1/policy", api_base_url.trim_end_matches('/'));
210
211 let client = if verify_tls {
212 reqwest::Client::new()
213 } else {
214 reqwest::Client::builder()
215 .danger_accept_invalid_certs(true)
216 .build()
217 .context("Failed to create HTTP client")?
218 };
219
220 let response = client
221 .post(&url)
222 .header("Authorization", format!("Bearer {}", bearer_token))
223 .json(&serde_json::json!({
224 "name": policy_name,
225 "address": policy_address
226 }))
227 .send()
228 .await
229 .with_context(|| format!("Failed to send request to {}", url))?;
230
231 let status = response.status();
232 if !status.is_success() {
233 let error_text = response.text().await.unwrap_or_default();
234 return Err(eyre::eyre!("API request failed with status {}: {}", status, error_text));
235 }
236
237 let policy_response: PolicyResponse = response.json().await.context("Failed to parse policy response")?;
238
239 Ok(policy_response.id)
240}
241
242pub async fn upload_policy_data_secret(
255 api_base_url: &str,
256 policy_data_id: &str,
257 key: &str,
258 encrypted_value: &str,
259 bearer_token: &str,
260 verify_tls: bool,
261) -> Result<()> {
262 let url = format!(
263 "{}/v1/policy-data/{}/secret",
264 api_base_url.trim_end_matches('/'),
265 policy_data_id
266 );
267
268 let client = if verify_tls {
269 reqwest::Client::new()
270 } else {
271 reqwest::Client::builder()
272 .danger_accept_invalid_certs(true)
273 .build()
274 .context("Failed to create HTTP client")?
275 };
276
277 let secret_request = SecretRequest {
278 key: key.to_string(),
279 value: encrypted_value.to_string(),
280 };
281
282 let response = client
283 .post(&url)
284 .header("Authorization", format!("Bearer {}", bearer_token))
285 .json(&secret_request)
286 .send()
287 .await
288 .with_context(|| format!("Failed to send request to {}", url))?;
289
290 let status = response.status();
291 if !status.is_success() {
292 let error_text = response.text().await.unwrap_or_default();
293 return Err(eyre::eyre!("API request failed with status {}: {}", status, error_text));
294 }
295
296 Ok(())
297}
298
299#[derive(Debug, Parser)]
301#[command(name = "newton-dashboard")]
302pub struct NewtonDashboardCommand {
303 #[command(subcommand)]
304 pub subcommand: NewtonDashboardSubcommand,
305}
306
307#[derive(Debug, Subcommand)]
308pub enum NewtonDashboardSubcommand {
309 Login(LoginCommand),
311 #[command(name = "upload-policy-and-secret")]
313 UploadPolicyAndSecret(UploadPolicyAndSecretCommand),
314}
315
316#[derive(Debug, Parser)]
318pub struct LoginCommand {
319 #[arg(long, default_value = "http://localhost:3000")]
321 dashboard_url: String,
322
323 #[arg(long, default_value = "http://localhost:8000")]
325 api_base_url: String,
326
327 #[arg(long, default_value = "3")]
329 poll_interval: u64,
330
331 #[arg(long, default_value = "300")]
333 max_poll_time: u64,
334}
335
336#[derive(Debug, Parser)]
338pub struct UploadPolicyAndSecretCommand {
339 #[arg(long)]
341 api_base_url: Option<String>,
342
343 #[arg(long)]
345 policy_name: String,
346
347 #[arg(long)]
349 policy_address: String,
350
351 #[arg(long)]
353 policy_data_address: String,
354
355 #[arg(long)]
357 secret_key: String,
358
359 #[arg(long)]
361 secret_value: String,
362
363 #[arg(long)]
365 bearer_token: Option<String>,
366
367 #[arg(long, default_value = "false")]
369 skip_tls_verify: bool,
370}
371
372async fn poll_for_token(
374 api_base_url: &str,
375 session_id: &str,
376 poll_interval: u64,
377 max_poll_time: u64,
378) -> eyre::Result<String> {
379 let url = format!(
380 "{}/v1/authn/cli-token/{}",
381 api_base_url.trim_end_matches('/'),
382 session_id
383 );
384
385 let client = reqwest::Client::new();
386 let start_time = std::time::Instant::now();
387 let max_duration = std::time::Duration::from_secs(max_poll_time);
388 let poll_interval_duration = std::time::Duration::from_secs(poll_interval);
389
390 info!("Polling for token (timeout: {}s)...", max_poll_time);
391
392 loop {
393 if start_time.elapsed() > max_duration {
394 return Err(eyre::eyre!(
395 "Login timeout: no token received within {} seconds. Please try again.",
396 max_poll_time
397 ));
398 }
399
400 let response = client.get(&url).send().await;
401
402 match response {
403 Ok(resp) if resp.status().is_success() => {
404 match resp.json::<CliTokenResponse>().await {
405 Ok(token_response) => {
406 match token_response.status.as_str() {
407 "completed" => {
408 if let Some(token) = token_response.token {
409 return Ok(token);
410 } else {
411 return Err(eyre::eyre!("Token marked as completed but no token provided"));
412 }
413 }
414 "expired" => {
415 return Err(eyre::eyre!("Login session expired. Please try again."));
416 }
417 "pending" => {
418 info!("Waiting for login... ({:.0}s elapsed)", start_time.elapsed().as_secs());
420 }
421 status => {
422 tracing::warn!("Unknown status: {}", status);
423 }
424 }
425 }
426 Err(e) => {
427 tracing::warn!("Failed to parse token response: {}", e);
428 }
429 }
430 }
431 Ok(resp) => {
432 tracing::warn!("Polling failed with status: {}", resp.status());
433 }
434 Err(e) => {
435 tracing::warn!("Polling request failed: {}", e);
436 }
437 }
438
439 tokio::time::sleep(poll_interval_duration).await;
440 }
441}
442
443fn save_config(config: &NewtonCliConfig, config_path: &std::path::Path) -> eyre::Result<()> {
445 let toml_string = toml::to_string_pretty(config).context("Failed to serialize config to TOML")?;
446
447 if let Some(parent) = config_path.parent() {
449 std::fs::create_dir_all(parent).with_context(|| format!("Failed to create config directory: {:?}", parent))?;
450 }
451
452 std::fs::write(config_path, toml_string)
453 .with_context(|| format!("Failed to write config file: {:?}", config_path))?;
454
455 Ok(())
456}
457
458impl NewtonDashboardCommand {
459 pub async fn execute(self: Box<Self>, mut config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
461 match self.subcommand {
462 NewtonDashboardSubcommand::Login(cmd) => {
463 info!("Starting login flow for Newton Dashboard...");
464
465 let session_id = uuid::Uuid::new_v4().to_string();
467
468 let dashboard_login_url = format!(
470 "{}/cli-login?session_id={}",
471 cmd.dashboard_url.trim_end_matches('/'),
472 session_id
473 );
474
475 info!("Opening browser to: {}", dashboard_login_url);
476 info!("Please log in via the browser using your email and OTP...");
477
478 if let Err(e) = open::that(&dashboard_login_url) {
480 tracing::warn!("Failed to open browser automatically: {}", e);
481 info!("Please manually open this URL in your browser:");
482 info!("{}", dashboard_login_url);
483 }
484
485 let token =
487 poll_for_token(&cmd.api_base_url, &session_id, cmd.poll_interval, cmd.max_poll_time).await?;
488
489 info!("Successfully received bearer token!");
490
491 config.service.set_dashboard_bearer_token(Some(token));
493 config
494 .service
495 .set_dashboard_api_base_url(Some(cmd.api_base_url.clone()));
496
497 let config_path = {
499 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
500 std::path::PathBuf::from(home).join(".newton").join("newton-cli.toml")
501 };
502
503 save_config(&config.service, &config_path)?;
504
505 info!("Bearer token saved to config file: {:?}", config_path);
506 info!("You can now use the 'upload-policy-and-secret' command without providing a bearer token.");
507
508 Ok(())
509 }
510 NewtonDashboardSubcommand::UploadPolicyAndSecret(cmd) => {
511 info!("Uploading policy and secret to Newton Dashboard...");
512
513 let bearer_token = cmd
515 .bearer_token
516 .or_else(|| config.service.dashboard_bearer_token.clone())
517 .ok_or_else(|| {
518 eyre::eyre!(
519 "Bearer token not found. Please provide --bearer-token or run 'newton-dashboard login' first."
520 )
521 })?;
522
523 let api_base_url = cmd
525 .api_base_url
526 .or_else(|| config.service.dashboard_api_base_url.clone())
527 .unwrap_or_else(|| "http://localhost:8000".to_string());
528
529 info!("Encrypting secret value");
531 let encrypted_value = encrypt_data(cmd.secret_value.as_bytes(), KMS_PUBLIC_KEY_PEM)
532 .with_context(|| "Failed to encrypt secret data")?;
533
534 info!("Secret encrypted successfully");
535
536 info!("Creating policy call: {}", cmd.policy_name);
538 let policy_id = create_policy_call(
539 &api_base_url,
540 &cmd.policy_name,
541 &cmd.policy_address,
542 &bearer_token,
543 !cmd.skip_tls_verify,
544 )
545 .await
546 .with_context(|| "Failed to create policy call")?;
547
548 info!("Policy call created with ID: {}", policy_id);
549
550 info!("Creating policy data: {}", cmd.policy_name);
552 let policy_data_id = create_policy_data(
553 &api_base_url,
554 &cmd.policy_name,
555 &cmd.policy_data_address,
556 &bearer_token,
557 !cmd.skip_tls_verify,
558 )
559 .await
560 .with_context(|| "Failed to create policy data")?;
561
562 info!("Policy data created with ID: {}", policy_data_id);
563
564 info!("Uploading secret with key: {}", cmd.secret_key);
566 upload_policy_data_secret(
567 &api_base_url,
568 &policy_data_id,
569 &cmd.secret_key,
570 &encrypted_value,
571 &bearer_token,
572 !cmd.skip_tls_verify,
573 )
574 .await
575 .with_context(|| "Failed to upload secret")?;
576
577 info!("Secret uploaded successfully!");
578 info!("Policy ID: {}", policy_id);
579 info!("Policy data ID: {}", policy_data_id);
580 info!("Secret Key: {}", cmd.secret_key);
581
582 Ok(())
583 }
584 }
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 const TEST_PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY-----
594MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy8Dbv8prpJ/0kKhlGeJY
595ozo2t60EG8L0561g13R29oVDmMytRDtpYRZxlYQbzhhJTV1bYgHyUwYj90fd6y0m
596UHHJ9ZVKyj6HCdbO0nbt9Z4Khd9m5niJ2STgGUF0Q4Kq0lz4TgFYq9czvHBNc2Hq
597dxFyPtkMDVDN7+auviBN+iHXj3pVKcQYBSyEbUGq+74Ceb5pdNCjBiWQrt40etnx
598yU4aiUxE8SMBcgZnutK5UTjOSawafuqfBc9wfYTxEXk8qZ9s2qHI0wMDQN/POIoc
599l9X5BWFX3ONjT9hCsBCdG7hm2XxKpnUbEUVyHNbVyHwIdJTCHNbRWwq7wym2MxYv
600VwIDAQAB
601-----END PUBLIC KEY-----"#;
602
603 #[test]
604 fn test_encrypt_data() {
605 let data = b"test data";
606 let result = encrypt_data(data, TEST_PUBLIC_KEY_PEM);
607 assert!(result.is_ok());
608 let encrypted = result.unwrap();
609 assert!(!encrypted.is_empty());
611 assert!(general_purpose::STANDARD.decode(&encrypted).is_ok());
613 }
614
615 #[test]
616 fn test_encrypt_data_empty() {
617 let data = b"";
618 let result = encrypt_data(data, TEST_PUBLIC_KEY_PEM);
619 assert!(result.is_ok());
620 }
621
622 #[test]
623 fn test_encrypt_data_large() {
624 let data = "x".repeat(150).into_bytes();
627 let result = encrypt_data(&data, TEST_PUBLIC_KEY_PEM);
628 assert!(result.is_ok());
629 }
630
631 #[test]
632 fn test_encrypt_data_invalid_key() {
633 let data = b"test data";
634 let invalid_key = "invalid key";
635 let result = encrypt_data(data, invalid_key);
636 assert!(result.is_err());
637 }
638
639 #[test]
640 fn test_secret_request_serialization() {
641 let request = SecretRequest {
642 key: "test_key".to_string(),
643 value: "encrypted_value".to_string(),
644 };
645 let json = serde_json::to_string(&request).unwrap();
646 assert!(json.contains("test_key"));
647 assert!(json.contains("encrypted_value"));
648 }
649
650 #[test]
651 fn test_policy_response_deserialization() {
652 let json = r#"{"id": "policy-123"}"#;
653 let response: PolicyResponse = serde_json::from_str(json).unwrap();
654 assert_eq!(response.id, "policy-123");
655 }
656}