use std::str::FromStr as _;
use shep_client::Client;
use shep_core::paths::ShepPaths;
use shep_core::permissions::{Grants, Permission};
use shep_core::protocol::{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::unknown_dog_message;
use super::grants::warn_if_unenforced;
use super::upgrade::upgrade_one;
use super::vet::{ManifestRead, VERSION_BUDGET, read_manifest};
use super::{DogEntry, connect_or_absent, dog_entry, 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)]
pub(crate) struct Applied {
pub entry: DogEntry,
pub enabled: bool,
pub waiting: Vec<String>,
}
#[derive(Debug)]
pub(crate) enum GrantRefusal {
Usage {
code: ExitCode,
message: String,
},
Config(ShepTomlError),
}
impl From<ShepTomlError> for GrantRefusal {
fn from(err: ShepTomlError) -> Self {
Self::Config(err)
}
}
#[cfg_attr(windows, allow(clippy::result_large_err))]
pub(crate) fn record_change(
paths: &ShepPaths,
change: Change,
name: &str,
permission: &str,
) -> Result<Applied, GrantRefusal> {
let usage = |message: String| GrantRefusal::Usage {
code: ExitCode::Usage,
message,
};
let permission = match Permission::from_str(permission) {
Ok(permission) => permission.to_string(),
Err(err) => return Err(usage(err.to_string())),
};
let cfg = ShepToml::read_only(&paths.daemon_config)?;
let binary = cfg.adopted_dog_path(name);
if binary.is_none() && !crate::dog::BUILT_IN_DOGS.contains(&name) {
return Err(GrantRefusal::Usage {
code: ExitCode::InvalidConfig,
message: unknown_dog_message(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 Err(usage(unreadable(name))),
Ok(read) => read.manifest().cloned(),
Err(refusal) => {
return Err(GrantRefusal::Usage {
code: ExitCode::InvalidConfig,
message: format!("{}: {refusal}", binary.display()),
});
}
},
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 Err(usage(not_declared(name, &permission, &declared)));
}
}
if matches!(cfg.grant_record(name), Ok(Some(_))) {
upgrade_one(paths, name)?;
}
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,
events: held.events.clone(),
},
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,
events: held.events.clone(),
},
};
if now != held {
cfg.set_grant_record(name, &now)?;
}
Ok(Applied {
entry: dog_entry(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(),
})
})
.map_err(|refusal: Refusal| match refusal {
Refusal::Config(err) => GrantRefusal::Config(err),
Refusal::Unrecorded => usage(unrecorded(name)),
})
}
#[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 applied = match record_change(paths, change, name, permission) {
Ok(applied) => applied,
Err(GrantRefusal::Usage { code, message }) => return streams.fail(code, &message),
Err(GrantRefusal::Config(err)) => return fail_config(streams, &err),
};
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.entry.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.entry.source.clone(),
env: applied.entry.env.clone(),
};
match client.request(request).await {
Ok(Response::DogStarted(_)) => {
warn_if_unenforced(
streams,
name,
&applied.entry.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"
)
}