use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tempfile::NamedTempFile;
use thiserror::Error;
pub const OPERATOR_SESSION_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OperatorSession {
pub version: u32,
pub pds_url: String,
pub operator_did: String,
pub operator_handle: String,
pub access_jwt: String,
pub refresh_jwt: String,
}
#[derive(Debug, Error)]
pub enum OperatorSessionError {
#[error("io: {0}")]
Io(#[from] io::Error),
#[error("operator session file {path} is malformed: {source}")]
Malformed {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error(
"operator session file {path} has unsupported version {found} (expected {expected}); re-run `cairn operator-login`"
)]
UnsupportedVersion {
path: PathBuf,
found: u32,
expected: u32,
},
#[error("{0}")]
CredentialFile(#[from] crate::credential_file::CredentialFileError),
}
impl OperatorSession {
pub fn load(path: &Path) -> Result<Option<Self>, OperatorSessionError> {
match fs::metadata(path) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
}
crate::credential_file::check_mode_and_owner(path)?;
let bytes = fs::read(path)?;
let session: OperatorSession =
serde_json::from_slice(&bytes).map_err(|source| OperatorSessionError::Malformed {
path: path.to_path_buf(),
source,
})?;
if session.version != OPERATOR_SESSION_VERSION {
return Err(OperatorSessionError::UnsupportedVersion {
path: path.to_path_buf(),
found: session.version,
expected: OPERATOR_SESSION_VERSION,
});
}
Ok(Some(session))
}
pub fn save(&self, path: &Path) -> Result<(), OperatorSessionError> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "session path has no parent")
})?;
fs::create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(parent, fs::Permissions::from_mode(0o700));
}
let mut tmp = NamedTempFile::new_in(parent)?;
let body = serde_json::to_vec_pretty(self).expect("OperatorSession serializes");
{
use std::io::Write as _;
tmp.write_all(&body)?;
tmp.as_file().sync_all()?;
}
tmp.persist(path).map_err(|e| e.error)?;
Ok(())
}
}
pub fn delete(path: &Path) -> Result<(), OperatorSessionError> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}