feat(credentials): support managed credential synchronization (#2490)

* feat(credentials): support managed credential synchronization

Allow managed callers to upsert credentials with an exact ID, secret,
permissions, reuse policy, and expiry.

Return non-secret attributes plus a public-key fingerprint so callers can
verify relay credential consistency.

Persist imported credentials atomically and preserve identity and expiry
across restarts.

* fix(credentials): make managed upserts durable

Write the candidate credential snapshot before committing it to memory.
Propagate storage failures so controllers can retry instead of observing
false convergence.

Cover a transient storage failure to verify that memory stays unchanged
and the retry persists the credential.

* fix(credentials): atomically replace stored snapshots

Define CredentialStorage::store as an atomic replacement boundary and
use atomic-write-file in the management adapter. This keeps the last
committed credential JSON readable when a replacement fails.

Cover replacement of an existing credential snapshot and keep the
dependency scoped to the management feature.
This commit is contained in:
KKRainbow
2026-08-10 23:20:36 +08:00
committed by GitHub
parent 23d55373a4
commit 0b27ac2885
7 changed files with 313 additions and 7 deletions
+7 -3
View File
@@ -1,5 +1,6 @@
use std::{path::PathBuf, sync::Arc};
use std::{io::Write, path::PathBuf, sync::Arc};
use atomic_write_file::AtomicWriteFile;
use easytier_core::peers::credential_manager::CredentialStorage;
struct FileCredentialStorage {
@@ -16,7 +17,9 @@ impl CredentialStorage for FileCredentialStorage {
}
fn store(&self, serialized_credentials: &str) -> anyhow::Result<()> {
std::fs::write(&self.path, serialized_credentials)?;
let mut file = AtomicWriteFile::open(&self.path)?;
file.write_all(serialized_credentials.as_bytes())?;
file.commit()?;
Ok(())
}
}
@@ -40,9 +43,10 @@ mod tests {
assert_eq!(storage.load().unwrap(), None);
storage.store("{\"credential\":true}").unwrap();
storage.store("{\"credential\":false}").unwrap();
assert_eq!(
storage.load().unwrap().as_deref(),
Some("{\"credential\":true}")
Some("{\"credential\":false}")
);
}
}