use std::str::FromStr as _;
use shep_client::Client;
use shep_core::paths::ShepPaths;
use shep_core::permissions::{Grants, Permission};
use shep_core::protocol::{DogSource, Request, Response};
use crate::commands::rpc::{client_error, unexpected_response};
use crate::commands::shep_toml::{GrantRecord, ShepToml, ShepTomlError};
use crate::exit::ExitCode;
use crate::output::{DogActionStatus, Streams};
use super::enable::fail_enable_unknown_dog;
use super::grants::warn_if_unenforced;
use super::upgrade::upgrade_one;
use super::vet::{ManifestRead, VERSION_BUDGET, fail_adopt, read_manifest};
use super::{connect_or_absent, dog_source, emit_dog_action, fail_config};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Change {
Grant,
Revoke,
}
impl Change {
const fn verb(self) -> &'static str {
match self {
Self::Grant => "grant",
Self::Revoke => "revoke",
}
}
}
#[derive(Debug)]
struct Applied {
source: DogSource,
enabled: bool,
waiting: Vec<String>,
}
#[derive(Debug)]
enum Refusal {
Config(ShepTomlError),
Unrecorded,
}
impl From<ShepTomlError> for Refusal {
fn from(err: ShepTomlError) -> Self {
Self::Config(err)
}
}
pub(crate) async fn change(
streams: &mut Streams<'_>,
paths: &ShepPaths,
change: Change,
name: &str,
permission: &str,
) -> ExitCode {
let permission = match Permission::from_str(permission) {
Ok(permission) => permission.to_string(),
Err(err) => return streams.fail(ExitCode::Usage, &err.to_string()),
};
let cfg = match ShepToml::read_only(&paths.daemon_config) {
Ok(cfg) => cfg,
Err(err) => return fail_config(streams, &err),
};
let binary = cfg.adopted_dog_path(name);
if binary.is_none() && !crate::dog::BUILT_IN_DOGS.contains(&name) {
return fail_enable_unknown_dog(streams, name, &cfg.adopted_dog_names());
}
if change == Change::Grant {
let manifest = match &binary {
Some(binary) => match read_manifest(binary, &paths.home, name, VERSION_BUDGET) {
Ok(ManifestRead::Unreadable) => {
return streams.fail(ExitCode::Usage, &unreadable(name));
}
Ok(read) => read.manifest().cloned(),
Err(refusal) => return fail_adopt(streams, binary, &refusal),
},
None => crate::dog::builtin_manifest(name),
};
let declared = manifest.map_or_else(Vec::new, |manifest| {
manifest
.permissions
.into_iter()
.map(|permission| permission.name)
.collect()
});
if !declared.contains(&permission) {
return streams.fail(ExitCode::Usage, ¬_declared(name, &permission, &declared));
}
}
if matches!(cfg.grant_record(name), Ok(Some(_)))
&& let Err(err) = upgrade_one(paths, name)
{
return fail_config(streams, &err);
}
let applied = ShepToml::try_edit(&paths.daemon_config, |cfg| {
let Some(held) = cfg.grant_record(name)? else {
return Err(Refusal::Unrecorded);
};
let without = |grants: &Grants| {
Grants::from_names(
grants
.names()
.filter(|kept| *kept != permission)
.map(str::to_owned),
)
};
let names = |grants: &Grants| grants.names().any(|held| held == permission);
let now = match change {
Change::Grant => GrantRecord {
granted: Grants::from_names(
held.granted
.names()
.map(str::to_owned)
.chain([permission.clone()]),
),
waiting: without(&held.waiting),
revoked: without(&held.revoked),
reviewed: true,
},
Change::Revoke if !names(&held.granted) && !names(&held.waiting) => held.clone(),
Change::Revoke => GrantRecord {
granted: without(&held.granted),
waiting: without(&held.waiting),
revoked: Grants::from_names(
held.revoked
.names()
.map(str::to_owned)
.chain([permission.clone()]),
),
reviewed: held.reviewed,
},
};
if now != held {
cfg.set_grant_record(name, &now)?;
}
Ok(Applied {
source: dog_source(cfg, name, Some(now.granted.clone())),
enabled: cfg.enabled_dog_names().iter().any(|dog| dog == name),
waiting: now.waiting.names().map(str::to_owned).collect(),
})
});
let applied = match applied {
Ok(applied) => applied,
Err(Refusal::Config(err)) => return fail_config(streams, &err),
Err(Refusal::Unrecorded) => return streams.fail(ExitCode::Usage, &unrecorded(name)),
};
let client = if applied.enabled {
match connect_or_absent(paths, streams).await {
Ok(client) => client,
Err(code) => return code,
}
} else {
None
};
if !applied.waiting.is_empty() {
streams.aside(
"dog_grants_waiting",
&format!(
"{name} still waits on {}: `shep dogs grant {name} <permission>` or `shep enable \
{name}` approves",
applied.waiting.join(", ")
),
);
}
reach_shepherd(streams, change, name, &applied, client.as_ref()).await
}
async fn reach_shepherd(
streams: &mut Streams<'_>,
change: Change,
name: &str,
applied: &Applied,
client: Option<&Client>,
) -> ExitCode {
let report = |streams: &mut Streams<'_>, status| {
emit_dog_action(streams, change.verb(), name, applied.source.clone(), status)
};
if !applied.enabled {
return report(
streams,
DogActionStatus::NoShepherd("recorded; the dog is disabled and gets it when enabled"),
);
}
let Some(client) = client else {
return report(
streams,
DogActionStatus::NoShepherd(
"recorded; takes effect when the next shepherd starts the dog",
),
);
};
let request = Request::EnableDog {
name: name.to_string(),
source: applied.source.clone(),
};
match client.request(request).await {
Ok(Response::DogStarted(_)) => {
warn_if_unenforced(streams, name, &applied.source, client.daemon().protocol);
report(
streams,
DogActionStatus::Acted("applied to the running dog"),
)
}
Ok(_unrecognised) => unexpected_response(streams),
Err(err) => client_error(streams, &err),
}
}
fn not_declared(name: &str, permission: &str, declared: &[String]) -> String {
if declared.is_empty() {
return format!(
"{name} declares no permissions, so `{permission}` cannot be granted to it"
);
}
let mut sorted = declared.to_vec();
sorted.sort();
format!(
"{name} did not declare `{permission}`, so it cannot be granted. It declares: {}",
sorted.join(", ")
)
}
fn unreadable(name: &str) -> String {
format!(
"{name}'s manifest record could not be read, so what it declares is unknown. \
Nothing was changed; the cache under `manifests/` in the shep home is safe to delete"
)
}
fn unrecorded(name: &str) -> String {
format!(
"{name} has no recorded grants: it either declares nothing and runs with full access, \
or was never asked. Nothing was changed; `shep enable {name}` lists its \
permissions and asks"
)
}