Skip to main content

dbx_tools_databricks_auth/storage/
file.rs

1use std::{
2    collections::HashMap,
3    fs::OpenOptions,
4    path::{Path, PathBuf},
5    time::{Duration, Instant},
6};
7
8use async_trait::async_trait;
9use fs4::fs_std::FileExt;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use tokio::io::AsyncWriteExt;
13
14use super::{CredentialStore, StorageLock};
15use crate::{Error, Result, Token};
16
17const TOKEN_CACHE_VERSION: u8 = 1;
18
19#[derive(Serialize, Deserialize)]
20struct TokenCache {
21    version: u8,
22    #[serde(default)]
23    tokens: HashMap<String, Token>,
24}
25
26impl Default for TokenCache {
27    fn default() -> Self {
28        Self {
29            version: TOKEN_CACHE_VERSION,
30            tokens: HashMap::new(),
31        }
32    }
33}
34
35pub struct FileStore {
36    root: PathBuf,
37    token_cache: PathBuf,
38}
39
40impl FileStore {
41    pub fn new(root: PathBuf) -> Result<Self> {
42        std::fs::create_dir_all(&root)?;
43        set_private_directory(&root)?;
44        Ok(Self {
45            token_cache: root.join("token-cache.json"),
46            root,
47        })
48    }
49
50    fn lock_path(&self, profile: &str) -> PathBuf {
51        self.root
52            .join("locks")
53            .join(format!("{}.lock", key_hash(profile)))
54    }
55
56    fn cache_lock_path(&self) -> PathBuf {
57        self.root.join("token-cache.lock")
58    }
59
60    pub(crate) async fn acquire_file_lock(
61        &self,
62        profile: &str,
63        timeout: Duration,
64    ) -> Result<FileLock> {
65        acquire_lock(self.lock_path(profile), profile.to_owned(), timeout).await
66    }
67
68    async fn acquire_cache_lock(&self) -> Result<FileLock> {
69        acquire_lock(
70            self.cache_lock_path(),
71            "token-cache.json".to_owned(),
72            Duration::from_secs(30),
73        )
74        .await
75    }
76
77    async fn read_cache(&self) -> Result<TokenCache> {
78        match tokio::fs::read(&self.token_cache).await {
79            Ok(raw) => {
80                let cache: TokenCache = serde_json::from_slice(&raw)?;
81                if cache.version != TOKEN_CACHE_VERSION {
82                    return Err(Error::Storage(format!(
83                        "token cache needs version {TOKEN_CACHE_VERSION}, got {}",
84                        cache.version
85                    )));
86                }
87                Ok(cache)
88            }
89            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(TokenCache::default()),
90            Err(error) => Err(error.into()),
91        }
92    }
93
94    async fn write_cache(&self, cache: &TokenCache) -> Result<()> {
95        let temporary = self
96            .root
97            .join(format!(".token-cache-{}.tmp", uuid::Uuid::new_v4()));
98        let raw = serde_json::to_vec_pretty(cache)?;
99        let mut file = tokio::fs::OpenOptions::new()
100            .create_new(true)
101            .write(true)
102            .open(&temporary)
103            .await?;
104        file.write_all(&raw).await?;
105        file.sync_all().await?;
106        drop(file);
107        set_private_file(&temporary)?;
108        tokio::fs::rename(&temporary, &self.token_cache).await?;
109        Ok(())
110    }
111}
112
113async fn acquire_lock(path: PathBuf, name: String, timeout: Duration) -> Result<FileLock> {
114    if let Some(parent) = path.parent() {
115        std::fs::create_dir_all(parent)?;
116        set_private_directory(parent)?;
117    }
118    tokio::task::spawn_blocking(move || {
119        let file = OpenOptions::new()
120            .create(true)
121            .truncate(false)
122            .read(true)
123            .write(true)
124            .open(path)?;
125        let deadline = Instant::now() + timeout;
126        loop {
127            if file.try_lock_exclusive()? {
128                return Ok(FileLock(file));
129            }
130            if Instant::now() >= deadline {
131                return Err(Error::LockTimeout(name));
132            }
133            std::thread::sleep(Duration::from_millis(50));
134        }
135    })
136    .await
137    .map_err(|error| Error::Storage(format!("file lock task failed: {error}")))?
138}
139
140pub(crate) struct FileLock(std::fs::File);
141impl StorageLock for FileLock {}
142
143impl Drop for FileLock {
144    fn drop(&mut self) {
145        let _ = fs4::fs_std::FileExt::unlock(&self.0);
146    }
147}
148
149#[async_trait]
150impl CredentialStore for FileStore {
151    async fn load(&self, profile: &str) -> Result<Option<Token>> {
152        let _lock = self.acquire_cache_lock().await?;
153        Ok(self.read_cache().await?.tokens.remove(profile))
154    }
155
156    async fn save(&self, profile: &str, token: &Token) -> Result<()> {
157        let _lock = self.acquire_cache_lock().await?;
158        let mut cache = self.read_cache().await?;
159        cache.tokens.insert(profile.to_owned(), token.clone());
160        self.write_cache(&cache).await
161    }
162
163    async fn delete(&self, profile: &str) -> Result<()> {
164        let _lock = self.acquire_cache_lock().await?;
165        let mut cache = self.read_cache().await?;
166        cache.tokens.remove(profile);
167        self.write_cache(&cache).await
168    }
169
170    async fn lock(&self, profile: &str, timeout: Duration) -> Result<Box<dyn StorageLock>> {
171        Ok(Box::new(self.acquire_file_lock(profile, timeout).await?))
172    }
173
174    fn name(&self) -> &'static str {
175        "file"
176    }
177}
178
179fn key_hash(profile: &str) -> String {
180    format!("{:x}", Sha256::digest(profile.as_bytes()))
181}
182
183#[cfg(unix)]
184fn set_private_file(path: &Path) -> Result<()> {
185    use std::os::unix::fs::PermissionsExt;
186    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
187    Ok(())
188}
189
190#[cfg(not(unix))]
191fn set_private_file(_path: &Path) -> Result<()> {
192    Ok(())
193}
194
195#[cfg(unix)]
196fn set_private_directory(path: &Path) -> Result<()> {
197    use std::os::unix::fs::PermissionsExt;
198    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
199    Ok(())
200}
201
202#[cfg(not(unix))]
203fn set_private_directory(_path: &Path) -> Result<()> {
204    Ok(())
205}