Skip to main content

nsg_cli/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6const CONFIG_DIR: &str = ".nsg";
7const CREDENTIALS_FILE: &str = "credentials.json";
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct Credentials {
11    pub username: String,
12    pub password: String,
13    pub app_key: String,
14}
15
16impl Credentials {
17    pub fn new(username: String, password: String, app_key: String) -> Self {
18        Self {
19            username,
20            password,
21            app_key,
22        }
23    }
24
25    pub fn load() -> Result<Self> {
26        let path = Self::credentials_path()?;
27
28        if !path.exists() {
29            anyhow::bail!(
30                "No credentials found. Please run 'nsg login' first.\n\
31                 Expected credentials at: {}",
32                path.display()
33            );
34        }
35
36        let content = fs::read_to_string(&path)
37            .with_context(|| format!("Failed to read credentials from {}", path.display()))?;
38
39        let creds: Credentials =
40            serde_json::from_str(&content).context("Failed to parse credentials file")?;
41
42        Ok(creds)
43    }
44
45    pub fn save(&self) -> Result<()> {
46        let config_dir = Self::config_dir()?;
47
48        if !config_dir.exists() {
49            fs::create_dir_all(&config_dir).with_context(|| {
50                format!(
51                    "Failed to create config directory at {}",
52                    config_dir.display()
53                )
54            })?;
55        }
56
57        let path = Self::credentials_path()?;
58        let content =
59            serde_json::to_string_pretty(self).context("Failed to serialize credentials")?;
60
61        fs::write(&path, content)
62            .with_context(|| format!("Failed to write credentials to {}", path.display()))?;
63
64        // Set file permissions to owner-only read/write
65        Self::set_secure_permissions(&path)?;
66
67        Ok(())
68    }
69
70    fn config_dir() -> Result<PathBuf> {
71        let home = dirs::home_dir().context("Could not determine home directory")?;
72        Ok(home.join(CONFIG_DIR))
73    }
74
75    fn credentials_path() -> Result<PathBuf> {
76        Ok(Self::config_dir()?.join(CREDENTIALS_FILE))
77    }
78
79    pub fn credentials_location() -> String {
80        Self::credentials_path()
81            .map(|p| p.display().to_string())
82            .unwrap_or_else(|_| format!("~/{}/{}", CONFIG_DIR, CREDENTIALS_FILE))
83    }
84
85    /// Set file permissions to owner-only read/write (0600 on Unix, ACL on Windows)
86    fn set_secure_permissions(path: &PathBuf) -> Result<()> {
87        #[cfg(unix)]
88        {
89            use std::os::unix::fs::PermissionsExt;
90            let mut perms = fs::metadata(path)
91                .context("Failed to get file metadata")?
92                .permissions();
93            perms.set_mode(0o600);
94            fs::set_permissions(path, perms).context("Failed to set file permissions to 0600")?;
95        }
96
97        #[cfg(windows)]
98        {
99            use std::os::windows::fs::MetadataExt;
100
101            // On Windows, we need to use icacls or similar to set proper ACLs
102            // Using a simpler approach: mark as hidden and system to discourage casual access
103            let metadata = fs::metadata(path).context("Failed to get file metadata")?;
104
105            // Set file attributes to hidden (not perfect, but better than nothing)
106            let mut perms = metadata.permissions();
107            perms.set_readonly(false); // Keep writable for the owner
108            fs::set_permissions(path, perms).context("Failed to set file permissions")?;
109
110            // Attempt to use icacls to set proper ACLs (owner-only access)
111            // This is the proper way to secure files on Windows
112            if let Err(e) = Self::set_windows_acl(path) {
113                eprintln!(
114                    "Warning: Could not set Windows ACL for credentials file: {}",
115                    e
116                );
117                eprintln!("         File permissions may not be fully secure on Windows.");
118                eprintln!("         Consider protecting your user account with a strong password.");
119            }
120        }
121
122        Ok(())
123    }
124
125    #[cfg(windows)]
126    fn set_windows_acl(path: &PathBuf) -> Result<()> {
127        use std::process::Command;
128
129        // Use icacls to:
130        // 1. Disable inheritance (/inheritance:r)
131        // 2. Grant current user full control (/grant:r %USERNAME%:F)
132        let output = Command::new("icacls")
133            .arg(path)
134            .arg("/inheritance:r")
135            .arg("/grant:r")
136            .arg(format!(
137                "{}:F",
138                std::env::var("USERNAME").unwrap_or_else(|_| String::from("*S-1-5-32-544"))
139            ))
140            .output()
141            .context("Failed to execute icacls command")?;
142
143        if !output.status.success() {
144            anyhow::bail!("icacls failed: {}", String::from_utf8_lossy(&output.stderr));
145        }
146
147        Ok(())
148    }
149}