Add offline resource capability discovery
This commit is contained in:
parent
116a431a12
commit
47d16eb714
6 changed files with 200 additions and 12 deletions
|
|
@ -483,7 +483,7 @@ pub enum Command {
|
|||
#[command(subcommand)]
|
||||
command: OverlayCommand,
|
||||
},
|
||||
/// List and create resource registrations
|
||||
/// List/create resources and discover authorization capabilities
|
||||
Resource {
|
||||
#[command(subcommand)]
|
||||
command: ResourceCommand,
|
||||
|
|
@ -974,6 +974,15 @@ pub enum ResourceCommand {
|
|||
List,
|
||||
/// Register a named resource
|
||||
Create { kind: String, name: String },
|
||||
/// List resource ID patterns and the capabilities they accept
|
||||
#[command(
|
||||
alias = "catalog",
|
||||
after_help = "Examples:\n geth resource capabilities\n geth resource capabilities kv\n geth resource capabilities --json"
|
||||
)]
|
||||
Capabilities {
|
||||
#[arg(help = "Optional resource family to describe, such as kv, ssh, or pipe")]
|
||||
family: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
@ -1831,6 +1840,13 @@ async fn run_inner(cli: Cli) -> Result<()> {
|
|||
print_completions(*shell);
|
||||
return Ok(());
|
||||
}
|
||||
if let Command::Resource {
|
||||
command: ResourceCommand::Capabilities { family },
|
||||
} = &cli.command
|
||||
{
|
||||
print_resource_capabilities(family.as_deref(), output)?;
|
||||
return Ok(());
|
||||
}
|
||||
if cli.home.is_some()
|
||||
&& matches!(
|
||||
&cli.command,
|
||||
|
|
@ -2292,6 +2308,82 @@ fn print_guide(topic: Option<GuideTopic>, output: OutputMode) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn resource_capability_contracts(
|
||||
family: Option<&str>,
|
||||
) -> Result<Vec<&'static geth_node::resource_contracts::ResourceModuleContract>> {
|
||||
let contracts = geth_node::resource_contracts::contracts();
|
||||
let Some(family) = family else {
|
||||
return Ok(contracts.iter().collect());
|
||||
};
|
||||
let canonical_family = match family {
|
||||
"cas" | "file-root" => "cas-file-root",
|
||||
"ssh-proxy" => "ssh",
|
||||
other => other,
|
||||
};
|
||||
let selected = contracts
|
||||
.iter()
|
||||
.filter(|contract| contract.family == canonical_family)
|
||||
.collect::<Vec<_>>();
|
||||
if selected.is_empty() {
|
||||
let available = contracts
|
||||
.iter()
|
||||
.map(|contract| contract.family)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
bail!(
|
||||
"invalid resource family {family:?}; available families: {available}\nnext: run `geth resource capabilities` to inspect the complete catalog"
|
||||
);
|
||||
}
|
||||
Ok(selected)
|
||||
}
|
||||
|
||||
fn print_resource_capabilities(family: Option<&str>, output: OutputMode) -> Result<()> {
|
||||
let contracts = resource_capability_contracts(family)?;
|
||||
if output.is_machine() {
|
||||
let families = contracts
|
||||
.iter()
|
||||
.map(|contract| {
|
||||
serde_json::json!({
|
||||
"family": contract.family,
|
||||
"resource_id_patterns": contract.resource_ids,
|
||||
"capabilities": contract.capabilities,
|
||||
"operations_and_host_effects": contract.mutation_points,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
output.print(&serde_json::json!({
|
||||
"type": "resource-capabilities",
|
||||
"families": families,
|
||||
"grant_example": "geth auth grant <subject> <resource-id> <capability> --signing-key <path>",
|
||||
"note": "replace angle-bracket placeholders; discovery does not grant access",
|
||||
}))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (index, contract) in contracts.iter().enumerate() {
|
||||
if index > 0 {
|
||||
println!();
|
||||
}
|
||||
println!("{}", contract.family);
|
||||
println!(" resource IDs:");
|
||||
for resource_id in contract.resource_ids {
|
||||
println!(" {resource_id}");
|
||||
}
|
||||
println!(" capabilities:");
|
||||
for capability in contract.capabilities {
|
||||
println!(" {capability}");
|
||||
}
|
||||
println!(" operations and host effects:");
|
||||
for point in contract.mutation_points {
|
||||
println!(" - {point}");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("grant: geth auth grant <subject> <resource-id> <capability> --signing-key <path>");
|
||||
println!("note: replace angle-bracket placeholders; discovery does not grant access");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_completions(shell: Shell) {
|
||||
let mut command = documented_cli_command();
|
||||
generate(shell, &mut command, "geth", &mut stdout());
|
||||
|
|
@ -2500,6 +2592,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
Command::Resource {
|
||||
command: ResourceCommand::Create { kind, name },
|
||||
} => ControlRequest::ResourceCreate { kind, name },
|
||||
Command::Resource {
|
||||
command: ResourceCommand::Capabilities { .. },
|
||||
} => bail!("resource capability discovery is handled directly by the CLI"),
|
||||
Command::Keychain {
|
||||
command:
|
||||
KeychainCommand::Init {
|
||||
|
|
@ -5487,6 +5582,30 @@ mod tests {
|
|||
assert!(Cli::try_parse_from(["geth", "status", "--json", "--jsonl"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_capabilities_are_available_without_daemon_state() {
|
||||
let all = resource_capability_contracts(None).expect("complete catalog");
|
||||
assert!(all.len() >= 8);
|
||||
let kv = resource_capability_contracts(Some("kv")).expect("kv catalog");
|
||||
assert_eq!(kv.len(), 1);
|
||||
assert_eq!(kv[0].resource_ids, &["resource:kv:<name>"]);
|
||||
assert!(kv[0].capabilities.contains(&"kv.write_prefix:<prefix>"));
|
||||
assert_eq!(
|
||||
resource_capability_contracts(Some("cas")).expect("cas alias")[0].family,
|
||||
"cas-file-root"
|
||||
);
|
||||
assert!(resource_capability_contracts(Some("unknown")).is_err());
|
||||
|
||||
let parsed = Cli::try_parse_from(["geth", "resource", "capabilities", "ssh"])
|
||||
.expect("parse capability discovery");
|
||||
assert!(matches!(
|
||||
parsed.command,
|
||||
Command::Resource {
|
||||
command: ResourceCommand::Capabilities { family: Some(family) }
|
||||
} if family == "ssh"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ephemeral_daemon_rejects_an_explicit_persistent_home() {
|
||||
let parsed = Cli::try_parse_from([
|
||||
|
|
|
|||
Loading…
Reference in a new issue