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 = serde_json::from_str(&content)
40            .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)
50                .with_context(|| format!("Failed to create config directory at {}", config_dir.display()))?;
51        }
52
53        let path = Self::credentials_path()?;
54        let content = serde_json::to_string_pretty(self)
55            .context("Failed to serialize credentials")?;
56
57        fs::write(&path, content)
58            .with_context(|| format!("Failed to write credentials to {}", path.display()))?;
59
60        // Set file permissions to owner-only read/write
61        Self::set_secure_permissions(&path)?;
62
63        Ok(())
64    }
65
66    fn config_dir() -> Result<PathBuf> {
67        let home = dirs::home_dir()
68            .context("Could not determine home directory")?;
69        Ok(home.join(CONFIG_DIR))
70    }
71
72    fn credentials_path() -> Result<PathBuf> {
73        Ok(Self::config_dir()?.join(CREDENTIALS_FILE))
74    }
75
76    pub fn credentials_location() -> String {
77        Self::credentials_path()
78            .map(|p| p.display().to_string())
79            .unwrap_or_else(|_| format!("~/{}/{}", CONFIG_DIR, CREDENTIALS_FILE))
80    }
81
82    /// Set file permissions to owner-only read/write (0600 on Unix, ACL on Windows)
83    fn set_secure_permissions(path: &PathBuf) -> Result<()> {
84        #[cfg(unix)]
85        {
86            use std::os::unix::fs::PermissionsExt;
87            let mut perms = fs::metadata(path)
88                .context("Failed to get file metadata")?
89                .permissions();
90            perms.set_mode(0o600);
91            fs::set_permissions(path, perms)
92                .context("Failed to set file permissions to 0600")?;
93        }
94
95        #[cfg(windows)]
96        {
97            use std::os::windows::fs::MetadataExt;
98
99            // On Windows, we need to use icacls or similar to set proper ACLs
100            // Using a simpler approach: mark as hidden and system to discourage casual access
101            let metadata = fs::metadata(path)
102                .context("Failed to get file metadata")?;
103
104            // Set file attributes to hidden (not perfect, but better than nothing)
105            let mut perms = metadata.permissions();
106            perms.set_readonly(false); // Keep writable for the owner
107            fs::set_permissions(path, perms)
108                .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!("Warning: Could not set Windows ACL for credentials file: {}", e);
114                eprintln!("         File permissions may not be fully secure on Windows.");
115                eprintln!("         Consider protecting your user account with a strong password.");
116            }
117        }
118
119        Ok(())
120    }
121
122    #[cfg(windows)]
123    fn set_windows_acl(path: &PathBuf) -> Result<()> {
124        use std::process::Command;
125
126        // Use icacls to:
127        // 1. Disable inheritance (/inheritance:r)
128        // 2. Grant current user full control (/grant:r %USERNAME%:F)
129        let output = Command::new("icacls")
130            .arg(path)
131            .arg("/inheritance:r")
132            .arg("/grant:r")
133            .arg(format!("{}:F", std::env::var("USERNAME").unwrap_or_else(|_| String::from("*S-1-5-32-544"))))
134            .output()
135            .context("Failed to execute icacls command")?;
136
137        if !output.status.success() {
138            anyhow::bail!(
139                "icacls failed: {}",
140                String::from_utf8_lossy(&output.stderr)
141            );
142        }
143
144        Ok(())
145    }
146}