use super::checks::{self, Checks};
use super::{store::Store, Overrides, Profile};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Request {
List,
Load(String),
Save {
name: String,
profile: Profile,
create: bool,
previous: Option<Profile>,
},
Connect {
name: String,
profile: Profile,
attest: bool,
accepted: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Reply {
Catalog {
names: Vec<String>,
root: String,
},
Loaded {
name: String,
profile: Profile,
checks: Checks,
},
}
#[derive(Debug)]
pub(crate) struct Prepared {
pub(crate) stored: Profile,
pub(crate) resolved: Profile,
}
fn first_use_neutral(p: &Profile) -> Profile {
let mut p = p.clone();
p.authorized_at_unix = None;
if let Some(sc) = &mut p.sc {
(sc.vmac, sc.device_uuid) = (None, None);
}
p
}
pub(crate) struct Service {
pub(crate) store: Result<Store, String>,
pub(crate) overrides: Overrides,
}
impl Service {
pub(crate) fn new(store: Result<Store, String>, overrides: Overrides) -> Self {
Self { store, overrides }
}
fn store(&self) -> Result<&Store, String> {
self.store.as_ref().map_err(Clone::clone)
}
pub(crate) fn catalog(&self) -> Result<Reply, String> {
let store = self.store()?;
Ok(Reply::Catalog {
names: store.list()?,
root: store.root().display().to_string(),
})
}
pub(crate) fn load(&self, name: &str) -> Result<Reply, String> {
let profile = self.store()?.load(name)?;
self.loaded(name, profile)
}
fn loaded(&self, name: &str, profile: Profile) -> Result<Reply, String> {
let checks = checks::inspect(&self.overrides.resolve(&profile)?);
Ok(Reply::Loaded {
name: name.into(),
profile,
checks,
})
}
pub(crate) fn save(
&self,
name: &str,
mut profile: Profile,
create: bool,
previous: Option<Profile>,
) -> Result<Reply, String> {
let store = self.store()?;
let stored = if create {
None
} else {
Some(store.load(name)?)
};
if !create && previous != stored {
return Err("profile changed on disk; reload before saving".into());
}
if let Some(sc) = &mut profile.sc {
for path in [&mut sc.ca, &mut sc.cert, &mut sc.key] {
if !path.as_os_str().is_empty() {
*path = std::path::absolute(&*path)
.map_err(|e| format!("cannot resolve {}: {e}", path.display()))?;
}
}
if let Some(kept) = stored.as_ref().and_then(|p| p.sc.as_ref()) {
sc.vmac = kept.vmac.or(sc.vmac);
sc.device_uuid = kept.device_uuid.or(sc.device_uuid);
}
sc.assign_identity()?;
}
self.overrides.resolve(&profile)?;
store.save(name, &profile, create)?;
self.loaded(name, profile)
}
pub(crate) fn workspace(&self, name: &str) -> Option<std::path::PathBuf> {
self.store.as_ref().ok()?.workspace(name).ok()
}
pub(crate) fn prepare(
&self,
name: &str,
displayed: &Profile,
attest: bool,
accepted: &[String],
) -> Result<Prepared, String> {
let store = self.store()?;
let mut profile = store.load(name)?;
if profile != *displayed {
return Err("profile changed on disk; reload and review it before connecting".into());
}
self.overrides.resolve(&profile)?;
let mut changed = false;
if let Some(sc) = &mut profile.sc {
changed |= sc.assign_identity()?;
}
let resolved = self.overrides.resolve(&profile)?;
let checks = checks::inspect(&resolved);
checks.refusal()?;
checks.unconfirmed(accepted)?;
if profile.authorized_at_unix.is_none() {
if !attest {
return Err("explicit authorization is required before first use".into());
}
let at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("cannot timestamp authorization: {e}"))?
.as_secs();
profile.authorized_at_unix = Some(at);
profile.authorized_safety = Some(profile.safety.clone());
changed = true;
}
if changed {
store.save(name, &profile, false)?;
let on_disk = store.load(name)?;
if on_disk.authorized_at_unix.is_none()
|| first_use_neutral(&on_disk) != first_use_neutral(&profile)
{
return Err(
"profile changed on disk; reload and review it before connecting".into(),
);
}
profile = on_disk;
}
store.workspace(name)?;
let resolved = self.overrides.resolve(&profile)?;
Ok(Prepared {
stored: profile,
resolved,
})
}
}