mirror of
https://github.com/Kruhlmann/punlock.git
synced 2026-02-05 02:29:25 +00:00
182 lines
6.8 KiB
Rust
182 lines
6.8 KiB
Rust
use std::{
|
|
os::unix::fs::PermissionsExt,
|
|
path::PathBuf,
|
|
process::{Command, Stdio},
|
|
sync::Arc,
|
|
};
|
|
|
|
use futures::{stream::FuturesUnordered, StreamExt, TryFutureExt};
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
use crate::{
|
|
bitwarden::Bitwarden,
|
|
config::PunlockConfigurationEntry,
|
|
statics::{self, HOME_DIRECTORY},
|
|
};
|
|
|
|
pub struct UnmountedSecretStore {
|
|
bitwarden: Bitwarden<String>,
|
|
}
|
|
|
|
impl UnmountedSecretStore {
|
|
pub fn new(bitwarden: Bitwarden<String>) -> Self {
|
|
Self { bitwarden }
|
|
}
|
|
}
|
|
|
|
impl UnmountedSecretStore {
|
|
pub async fn into_platform_store(self) -> anyhow::Result<UnixSecretStore> {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(target_os = "linux")] {
|
|
let root_path = statics::RUNTIME_DIRECTORY.join("punlock");
|
|
let store = UnixSecretStore::new(self.bitwarden, root_path).teardown().await?.setup().await?;
|
|
Ok(store)
|
|
} else if #[cfg(target_os = "macos")] {
|
|
// mount_ramdisk_macos(mount_point)?;
|
|
panic!("todo");
|
|
} else {
|
|
panic!("todo");
|
|
// debug!("On Windows or unsupported OS: using plain dir at {}", mount_point.display());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct UnixSecretStore {
|
|
bitwarden: Arc<Bitwarden<String>>,
|
|
root_path: Arc<PathBuf>,
|
|
}
|
|
|
|
impl UnixSecretStore {
|
|
pub fn new(bitwarden: Bitwarden<String>, root_path: PathBuf) -> Self {
|
|
Self {
|
|
bitwarden: Arc::new(bitwarden),
|
|
root_path: Arc::new(root_path),
|
|
}
|
|
}
|
|
|
|
pub async fn write_secrets(&self, entries: &[PunlockConfigurationEntry]) -> anyhow::Result<()> {
|
|
let mut tasks = FuturesUnordered::new();
|
|
Command::new("bw")
|
|
.args(["sync", "--session", &self.bitwarden.session])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.inspect_err(|error| tracing::error!(?error, "spawn get"))?;
|
|
|
|
for entry in entries.iter() {
|
|
let root = self.root_path.clone();
|
|
let bw = self.bitwarden.clone();
|
|
|
|
tasks.push(
|
|
async move {
|
|
let secret = bw.fetch(entry).await.inspect_err(|error| tracing::error!(?error, ?entry, "item not found"))?;
|
|
let path = root.join(&entry.path);
|
|
|
|
tokio::fs::create_dir_all(path.parent().unwrap_or(&path)).await?;
|
|
{
|
|
let mut file = tokio::fs::File::create(&path).await?;
|
|
file.write_all(secret.as_bytes()).await?;
|
|
if !secret.ends_with('\n') {
|
|
file.write_all(b"\n").await?;
|
|
}
|
|
file.flush().await?;
|
|
}
|
|
|
|
let mut perms = tokio::fs::metadata(&path).await?.permissions();
|
|
perms.set_readonly(true);
|
|
|
|
if !entry.public {
|
|
#[cfg(unix)]
|
|
perms.set_mode(0o400);
|
|
}
|
|
tokio::fs::set_permissions(&path, perms)
|
|
.await
|
|
.inspect(|_| tracing::debug!(?path, "set readonly"))
|
|
.inspect_err(|error| tracing::error!(?error, ?path, "remove runtime dir"))?;
|
|
|
|
if let Some(ref links) = entry.links {
|
|
for link in links {
|
|
let link_path: PathBuf = if PathBuf::from(link).is_absolute() {
|
|
PathBuf::from(link)
|
|
} else {
|
|
HOME_DIRECTORY.join(link)
|
|
};
|
|
tokio::fs::create_dir_all(link_path.parent().unwrap_or(&link_path))
|
|
.await?;
|
|
match tokio::fs::symlink_metadata(&link_path).await {
|
|
Ok(meta) if meta.file_type().is_symlink() => {
|
|
let current = tokio::fs::read_link(&link_path).await?;
|
|
if current == path {
|
|
tracing::debug!(?link_path, "skipping existing symlink");
|
|
continue;
|
|
}
|
|
tokio::fs::remove_file(&link_path).await?;
|
|
}
|
|
Ok(_) => tokio::fs::remove_file(&link_path).await?,
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
Err(e) => return Err(e.into()),
|
|
}
|
|
|
|
let src = path.clone();
|
|
let dst = link_path.clone();
|
|
tokio::task::spawn_blocking(move || -> std::io::Result<()> {
|
|
#[cfg(unix)]
|
|
std::os::unix::fs::symlink(&src, &dst)?;
|
|
#[cfg(windows)]
|
|
std::os::windows::fs::symlink_file(&src, &dst)?;
|
|
Ok(())
|
|
})
|
|
.inspect_err(|error| tracing::error!(src = ?path, dst = ?link_path, ?error, "symlink failed"))
|
|
.await
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))??;
|
|
|
|
tracing::info!(src =? path, destination = ?link_path, "created/updated symlink");
|
|
}
|
|
}
|
|
|
|
Ok::<_, anyhow::Error>((entry.id.clone(), entry.path.clone()))
|
|
}
|
|
);
|
|
}
|
|
|
|
let mut count = 0;
|
|
let mut success = 0;
|
|
while let Some(res) = tasks.next().await {
|
|
count += 1;
|
|
match res {
|
|
Ok((id, path)) => {
|
|
success += 1;
|
|
tracing::info!(?id, ?path, "secret written")
|
|
}
|
|
Err(error) => tracing::error!(?error, "failed to write secret"),
|
|
}
|
|
}
|
|
|
|
tracing::info!("wrote {success}/{count} secrets");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn teardown(self) -> anyhow::Result<Self> {
|
|
if self.root_path.exists() {
|
|
tokio::fs::remove_dir_all(&*self.root_path)
|
|
.await
|
|
.inspect_err(
|
|
|error| tracing::error!(?error, path = ?self.root_path, "remove runtime dir"),
|
|
)
|
|
.ok();
|
|
}
|
|
Ok(self)
|
|
}
|
|
|
|
async fn setup(self) -> anyhow::Result<Self> {
|
|
tokio::fs::create_dir_all(&*self.root_path)
|
|
.await
|
|
.inspect_err(
|
|
|error| tracing::error!(?error, path = ?self.root_path, "create runtime dir"),
|
|
)?;
|
|
Ok(self)
|
|
}
|
|
}
|