use std::path::Path;
use shep_core::paths::ShepPaths;
use shep_core::permissions::Grants;
use crate::commands::shep_toml::{GrantRecord, ShepToml, ShepTomlError};
use super::consent::{Review, review};
use super::vet::{ManifestRead, VERSION_BUDGET, read_manifest};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Declaration {
Undeclared,
Declares(Grants),
Unknown,
}
pub(super) fn plan(
current: Option<&GrantRecord>,
enabled: bool,
declaration: &Declaration,
) -> Option<GrantRecord> {
let Declaration::Declares(declared) = declaration else {
return None;
};
let Some(record) = current else {
if !enabled {
return None;
}
return Some(if declared.names().next().is_none() {
GrantRecord::reviewed(declared.clone())
} else {
GrantRecord::at_upgrade(declared.clone())
});
};
let held = |name: &&str| record.granted.names().any(|g| g == *name);
let revoked = |name: &&str| record.revoked.names().any(|r| r == *name);
let waiting = Grants::from_names(
declared
.names()
.filter(|name| !held(name) && !revoked(name))
.map(str::to_owned),
);
let revoked = Grants::from_names(
record
.revoked
.names()
.filter(|name| declared.names().any(|d| d == *name))
.map(str::to_owned),
);
let next = GrantRecord {
waiting,
revoked,
..record.clone()
};
(next != *record).then_some(next)
}
pub(super) fn declaration_of(cfg: &ShepToml, home: &Path, name: &str) -> Declaration {
let manifest = match cfg.adopted_dog_path(name) {
Some(binary) => match read_manifest(&binary, home, name, VERSION_BUDGET) {
Ok(ManifestRead::Read(manifest)) => Some(manifest),
Ok(ManifestRead::Absent) => None,
Ok(ManifestRead::Unreadable) | Err(_) => return Declaration::Unknown,
},
None => crate::dog::builtin_manifest(name),
};
match review(manifest.as_ref()) {
Ok(Review::Undeclared) => Declaration::Undeclared,
Ok(Review::Declared { listed, .. }) => Declaration::Declares(Grants::from_names(
listed.iter().map(|entry| entry.name.clone()),
)),
Err(_) => Declaration::Unknown,
}
}
fn changes(cfg: &ShepToml, examined: &[(String, Declaration)]) -> Vec<(String, GrantRecord)> {
let enabled = cfg.enabled_dog_names();
examined
.iter()
.filter_map(|(name, declaration)| {
let current = cfg.grant_record(name).ok()?;
plan(
current.as_ref(),
enabled.iter().any(|dog| dog == name),
declaration,
)
.map(|record| (name.clone(), record))
})
.collect()
}
pub(crate) fn upgrade_grants(paths: &ShepPaths) -> Result<Vec<String>, ShepTomlError> {
let cfg = ShepToml::read_only(&paths.daemon_config)?;
let enabled = cfg.enabled_dog_names();
let mut candidates: Vec<String> = crate::dog::BUILT_IN_DOGS
.iter()
.map(|name| (*name).to_owned())
.chain(cfg.adopted_dog_names())
.collect();
candidates.sort();
candidates.dedup();
let examined: Vec<(String, Declaration)> = candidates
.into_iter()
.filter(|name| enabled.contains(name) || matches!(cfg.grant_record(name), Ok(Some(_))))
.map(|name| {
let declaration = declaration_of(&cfg, &paths.home, &name);
(name, declaration)
})
.collect();
if changes(&cfg, &examined).is_empty() {
return Ok(Vec::new());
}
ShepToml::try_edit(&paths.daemon_config, |cfg| {
let changed = changes(cfg, &examined);
for (name, record) in &changed {
cfg.set_grant_record(name, record)?;
}
Ok(changed.into_iter().map(|(name, _)| name).collect())
})
}
pub(super) fn upgrade_one(paths: &ShepPaths, name: &str) -> Result<(), ShepTomlError> {
let cfg = ShepToml::read_only(&paths.daemon_config)?;
let known = crate::dog::BUILT_IN_DOGS.contains(&name) || cfg.adopted_dog_path(name).is_some();
if !known {
return Ok(());
}
let examined = vec![(name.to_owned(), declaration_of(&cfg, &paths.home, name))];
if changes(&cfg, &examined).is_empty() {
return Ok(());
}
ShepToml::try_edit(&paths.daemon_config, |cfg| {
for (name, record) in changes(cfg, &examined) {
cfg.set_grant_record(&name, &record)?;
}
Ok(())
})
}
#[must_use]
pub(crate) fn upgrade_notice(changed: &[String]) -> String {
format!(
"updated the grants recorded for {}: `shep dogs` shows any permission waiting for \
`shep enable` or `shep dogs grant`, and any list not yet reviewed",
changed.join(", ")
)
}