From bf8612d04a13c5014895b7b43b746ca18c0a1c6b Mon Sep 17 00:00:00 2001 From: Victor Peter Date: Sat, 10 Oct 2026 09:59:46 +0200 Subject: [PATCH] Only list MCP tools the configured macaroon can call Scoped macaroons may only be allowed to call a few RPCs, but the MCP server listed every tool, wasting model context and inviting calls the server would deny. On startup, call get_permissions and only list tools whose RPC the macaroon's effective permissions and method caveats allow. Each tool spec now names the RPC it calls explicitly, and filtering uses that. Admin macaroons still see every tool and get_permissions is always listed. If permissions cannot be fetched we warn and list every tool as before. Hidden tools still route to the server so stale clients get the server's permission error. To share one RPC-to-permission table between the server and the MCP client, move method_authorization from the server into ldk-server-grpc's permissions module. The mapping is unchanged and the server's contract test still covers it. The startup probe now uses get_permissions instead of get_node_info, which avoids a misleading warning for macaroons without node:read. AI tools were used to help write this change and its tests. --- CONTRIBUTING.md | 6 +- e2e-tests/tests/mcp.rs | 14 ++ ldk-server-grpc/src/permissions.rs | 106 ++++++++++ ldk-server-mcp/README.md | 4 + ldk-server-mcp/src/main.rs | 23 ++- ldk-server-mcp/src/tools/mod.rs | 231 +++++++++++++++++++++- ldk-server/src/macaroons/authorization.rs | 106 +--------- 7 files changed, 371 insertions(+), 119 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc8c1f9d..8c819f36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,8 +56,10 @@ cargo fmt --all 2. Regenerate protos (see above) 3. Create handler in `ldk-server/src/api/` (follow existing patterns) 4. Add route in `ldk-server/src/service.rs` -5. Map the RPC to its required permission in `method_authorization` in `ldk-server/src/macaroons/authorization.rs`. - Unmapped methods return `UNIMPLEMENTED`, even for admin tokens. +5. Map the RPC to its required permission in `method_authorization` in + `ldk-server-grpc/src/permissions.rs` and update the test table in + `ldk-server/src/macaroons/authorization.rs`. Unmapped methods return `UNIMPLEMENTED`, even for + admin tokens, and are not listed by the MCP server. 6. Add CLI command in `ldk-server-cli/src/main.rs` 7. For a unary RPC, add the MCP tool in `ldk-server-mcp/src/tools/` and update the tool list test in `ldk-server-mcp/tests/integration.rs`. Add a live test in `e2e-tests/tests/mcp.rs` if applicable. diff --git a/e2e-tests/tests/mcp.rs b/e2e-tests/tests/mcp.rs index 119d9c23..66650c58 100644 --- a/e2e-tests/tests/mcp.rs +++ b/e2e-tests/tests/mcp.rs @@ -47,6 +47,20 @@ async fn test_mcp_macaroon_lifecycle_and_error_categories() { assert!(listed["macaroons"].as_array().unwrap().iter().any(|key| key["id"] == id)); assert!(!listed.to_string().contains(secret)); let mut reader = McpHandle::start_with_macaroon(&server, secret); + let tools = reader.call(0, "tools/list", json!({})); + let mut tool_names: Vec<_> = tools["result"]["tools"] + .as_array() + .unwrap() + .iter() + .map(|tool| tool["name"].as_str().unwrap()) + .collect(); + tool_names.sort(); + assert_eq!( + tool_names, + ["export_pathfinding_scores", "get_balances", "get_node_info", "get_permissions"] + ); + let admin_tools = admin.call(4, "tools/list", json!({})); + assert!(admin_tools["result"]["tools"].as_array().unwrap().len() > tool_names.len()); let permissions = reader.call(1, "tools/call", json!({"name": "get_permissions", "arguments": {}})); let permissions = tool_result_json(&permissions); diff --git a/ldk-server-grpc/src/permissions.rs b/ldk-server-grpc/src/permissions.rs index fd1a45f2..74451af3 100644 --- a/ldk-server-grpc/src/permissions.rs +++ b/ldk-server-grpc/src/permissions.rs @@ -7,6 +7,27 @@ // You may not use this file except in accordance with one or both of these // licenses. +use crate::endpoints::{ + BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, + BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_FOR_HASH_PATH, + BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, + BOLT11_RECEIVE_VIA_JIT_CHANNEL_FOR_HASH_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, + BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, BOLT12_CREATE_PAYER_PROOF_PATH, + BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, + BUMP_CHANNEL_FUNDING_FEE_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, CREATE_MACAROON_PATH, + DECODE_INVOICE_PATH, DECODE_OFFER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, + FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_CHANNEL_FORWARDING_STATS_PATH, + GET_FORWARDED_PAYMENT_DETAILS_PATH, GET_FORWARDED_PAYMENT_TRACKING_MODE_PATH, + GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, GET_PERMISSIONS_PATH, GRAPH_GET_CHANNEL_PATH, + GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, LIST_CHANNELS_PATH, + LIST_CHANNEL_FORWARDING_STATS_PATH, LIST_CHANNEL_PAIR_FORWARDING_STATS_PATH, + LIST_FORWARDED_PAYMENTS_PATH, LIST_MACAROONS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, + ONCHAIN_BUMP_FEE_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, + REVOKE_MACAROON_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, + SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, + VERIFY_SIGNATURE_PATH, +}; + pub const ADMIN_PERMISSION: &str = "admin"; pub const NODE_READ_PERMISSION: &str = "node:read"; pub const ONCHAIN_RECEIVE_PERMISSION: &str = "onchain:receive"; @@ -117,3 +138,88 @@ impl std::str::FromStr for MacaroonPreset { Self::ALL.into_iter().find(|preset| preset.name() == name).ok_or("Unknown macaroon preset") } } + +/// How an RPC method is authorized. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MethodAuthorization { + /// The caller needs this permission, or `admin`. + Permission(&'static str), + /// Any authenticated caller may use the method. + AuthenticatedOnly, + /// The method is not known to the server and is rejected. + Unknown, +} + +/// Returns how `method`, an RPC name such as [`GET_NODE_INFO_PATH`], is authorized. +/// +/// This is the mapping the server enforces, so clients can use it to tell which RPCs a +/// macaroon's permissions allow. +pub fn method_authorization(method: &str) -> MethodAuthorization { + match method { + GET_NODE_INFO_PATH | GET_BALANCES_PATH | EXPORT_PATHFINDING_SCORES_PATH => { + MethodAuthorization::Permission(NODE_READ_PERMISSION) + }, + ONCHAIN_RECEIVE_PATH => MethodAuthorization::Permission(ONCHAIN_RECEIVE_PERMISSION), + ONCHAIN_SEND_PATH | ONCHAIN_BUMP_FEE_PATH => { + MethodAuthorization::Permission(ONCHAIN_SEND_PERMISSION) + }, + BOLT11_RECEIVE_PATH + | BOLT11_RECEIVE_FOR_HASH_PATH + | BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH + | BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH + | BOLT11_RECEIVE_VIA_JIT_CHANNEL_FOR_HASH_PATH + | BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_FOR_HASH_PATH + | BOLT12_RECEIVE_PATH + | BOLT12_RECEIVE_REFUND_PATH => MethodAuthorization::Permission(INVOICES_CREATE_PERMISSION), + BOLT11_CLAIM_FOR_ID_PATH | BOLT11_FAIL_FOR_ID_PATH => { + MethodAuthorization::Permission(PAYMENTS_CLAIM_PERMISSION) + }, + BOLT11_SEND_PATH + | BOLT11_SEND_UNDERPAYING_PATH + | BOLT12_SEND_PATH + | BOLT12_SEND_REFUND_PATH + | SPONTANEOUS_SEND_PATH + | UNIFIED_SEND_PATH + | SPLICE_OUT_PATH => MethodAuthorization::Permission(PAYMENTS_SEND_PERMISSION), + GET_PAYMENT_DETAILS_PATH + | LIST_PAYMENTS_PATH + | LIST_FORWARDED_PAYMENTS_PATH + | GET_FORWARDED_PAYMENT_DETAILS_PATH + | GET_FORWARDED_PAYMENT_TRACKING_MODE_PATH + | GET_CHANNEL_FORWARDING_STATS_PATH + | LIST_CHANNEL_FORWARDING_STATS_PATH + | LIST_CHANNEL_PAIR_FORWARDING_STATS_PATH => { + MethodAuthorization::Permission(PAYMENTS_READ_PERMISSION) + }, + LIST_CHANNELS_PATH => MethodAuthorization::Permission(CHANNELS_READ_PERMISSION), + OPEN_CHANNEL_PATH + | UPDATE_CHANNEL_CONFIG_PATH + | CLOSE_CHANNEL_PATH + | SPLICE_IN_PATH + | BUMP_CHANNEL_FUNDING_FEE_PATH => MethodAuthorization::Permission(CHANNELS_MANAGE_PERMISSION), + FORCE_CLOSE_CHANNEL_PATH => { + MethodAuthorization::Permission(CHANNELS_FORCE_CLOSE_PERMISSION) + }, + LIST_PEERS_PATH => MethodAuthorization::Permission(PEERS_READ_PERMISSION), + CONNECT_PEER_PATH | DISCONNECT_PEER_PATH => { + MethodAuthorization::Permission(PEERS_MANAGE_PERMISSION) + }, + SIGN_MESSAGE_PATH | BOLT12_CREATE_PAYER_PROOF_PATH => { + MethodAuthorization::Permission(MESSAGES_SIGN_PERMISSION) + }, + VERIFY_SIGNATURE_PATH => MethodAuthorization::Permission(MESSAGES_VERIFY_PERMISSION), + GRAPH_LIST_CHANNELS_PATH + | GRAPH_GET_CHANNEL_PATH + | GRAPH_LIST_NODES_PATH + | GRAPH_GET_NODE_PATH => MethodAuthorization::Permission(GRAPH_READ_PERMISSION), + DECODE_INVOICE_PATH | DECODE_OFFER_PATH => { + MethodAuthorization::Permission(UTILITIES_READ_PERMISSION) + }, + SUBSCRIBE_EVENTS_PATH => MethodAuthorization::Permission(EVENTS_READ_PERMISSION), + CREATE_MACAROON_PATH | LIST_MACAROONS_PATH | REVOKE_MACAROON_PATH => { + MethodAuthorization::Permission(MACAROONS_MANAGE_PERMISSION) + }, + GET_PERMISSIONS_PATH => MethodAuthorization::AuthenticatedOnly, + _ => MethodAuthorization::Unknown, + } +} diff --git a/ldk-server-mcp/README.md b/ldk-server-mcp/README.md index ae765c66..8ff4e090 100644 --- a/ldk-server-mcp/README.md +++ b/ldk-server-mcp/README.md @@ -95,6 +95,10 @@ Add to your Claude Code MCP settings (`.claude/settings.json`): All unary LDK Server RPCs are exposed as MCP tools. Use `tools/list` to discover the current set. +On startup the server calls `get_permissions` and only lists the tools the configured macaroon is +allowed to call, so a restricted macaroon is not offered tools the server would deny. If the +permissions cannot be fetched, every tool is listed. + Streaming RPCs such as `subscribe_events` and non-RPC HTTP endpoints such as `metrics` are not exposed as tools. The `create_macaroon` tool returns a private token that may be saved in chat history or tool logs. diff --git a/ldk-server-mcp/src/main.rs b/ldk-server-mcp/src/main.rs index fd317b68..556a4d7c 100644 --- a/ldk-server-mcp/src/main.rs +++ b/ldk-server-mcp/src/main.rs @@ -13,7 +13,7 @@ mod protocol; mod tools; use ldk_server_client::client::LdkServerClient; -use ldk_server_client::ldk_server_grpc::api::GetNodeInfoRequest; +use ldk_server_client::ldk_server_grpc::api::GetPermissionsRequest; use serde_json::Value; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -60,16 +60,21 @@ async fn main() { }, }; - // Probe the server so misconfiguration surfaces on startup rather than on - // the first tool call. We warn instead of exiting so the MCP protocol loop - // still answers `initialize` and `tools/list` even when the server is - // temporarily unreachable. - if let Err(e) = client.get_node_info(GetNodeInfoRequest {}).await { - eprintln!("Warning: Failed to reach ldk-server on startup: {e}"); + let mut registry = build_tool_registry(); + + // Ask the server what this macaroon may do, and only list the tools it can call so the model + // is not offered tools that would be denied. This also surfaces misconfiguration on startup + // rather than on the first tool call. On failure we warn and list every tool instead of + // exiting, so the MCP protocol loop still answers `initialize` and `tools/list` even when the + // server is temporarily unreachable. + match client.get_permissions(GetPermissionsRequest {}).await { + Ok(response) => match response.macaroon { + Some(macaroon) => registry.retain_allowed_tools(&macaroon), + None => eprintln!("Warning: ldk-server returned no macaroon permissions"), + }, + Err(e) => eprintln!("Warning: Failed to reach ldk-server on startup: {e}"), } - let registry = build_tool_registry(); - eprintln!("ldk-server-mcp: ready, waiting for JSON-RPC requests on stdin"); let stdin = tokio::io::stdin(); diff --git a/ldk-server-mcp/src/tools/mod.rs b/ldk-server-mcp/src/tools/mod.rs index 49f9422b..49b2d68a 100644 --- a/ldk-server-mcp/src/tools/mod.rs +++ b/ldk-server-mcp/src/tools/mod.rs @@ -15,6 +15,29 @@ use std::future::Future; use std::pin::Pin; use ldk_server_client::client::LdkServerClient; +use ldk_server_client::ldk_server_grpc::api::MacaroonInfo; +use ldk_server_client::ldk_server_grpc::endpoints::{ + BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, + BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_FOR_HASH_PATH, + BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, + BOLT11_RECEIVE_VIA_JIT_CHANNEL_FOR_HASH_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, + BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, BOLT12_CREATE_PAYER_PROOF_PATH, + BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, + BUMP_CHANNEL_FUNDING_FEE_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, CREATE_MACAROON_PATH, + DECODE_INVOICE_PATH, DECODE_OFFER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, + FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_CHANNEL_FORWARDING_STATS_PATH, + GET_FORWARDED_PAYMENT_DETAILS_PATH, GET_FORWARDED_PAYMENT_TRACKING_MODE_PATH, + GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, GET_PERMISSIONS_PATH, GRAPH_GET_CHANNEL_PATH, + GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, LIST_CHANNELS_PATH, + LIST_CHANNEL_FORWARDING_STATS_PATH, LIST_CHANNEL_PAIR_FORWARDING_STATS_PATH, + LIST_FORWARDED_PAYMENTS_PATH, LIST_MACAROONS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, + ONCHAIN_BUMP_FEE_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, + REVOKE_MACAROON_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, + SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH, +}; +use ldk_server_client::ldk_server_grpc::permissions::{ + method_authorization, MethodAuthorization, ADMIN_PERMISSION, +}; use serde_json::Value; use crate::mcp::{ToolCallResult, ToolDefinition}; @@ -28,21 +51,24 @@ type ToolHandler = for<'a> fn( pub struct ToolRegistry { definitions: Vec, + rpc_methods: HashMap<&'static str, &'static str>, handlers: HashMap<&'static str, ToolHandler>, } struct ToolSpec { name: &'static str, + /// The RPC this tool calls, used to decide whether a macaroon may see it. + rpc_method: &'static str, description: &'static str, input_schema: fn() -> Value, handler: ToolHandler, } fn tool_spec( - name: &'static str, description: &'static str, input_schema: fn() -> Value, - handler: ToolHandler, + name: &'static str, rpc_method: &'static str, description: &'static str, + input_schema: fn() -> Value, handler: ToolHandler, ) -> ToolSpec { - ToolSpec { name, description, input_schema, handler } + ToolSpec { name, rpc_method, description, input_schema, handler } } impl ToolRegistry { @@ -50,6 +76,20 @@ impl ToolRegistry { &self.definitions } + /// Stops listing tools the given macaroon is not allowed to call. + /// + /// `macaroon` is the caller's own details as returned by `get_permissions`, with its + /// permissions already narrowed by its caveats. Hidden tools stay callable so that a stale + /// list still gets the server's permission error rather than an unknown tool error. + pub fn retain_allowed_tools(&mut self, macaroon: &MacaroonInfo) { + let rpc_methods = &self.rpc_methods; + self.definitions.retain(|tool| { + rpc_methods + .get(tool.name.as_str()) + .is_some_and(|method| macaroon_allows(macaroon, method)) + }); + } + pub async fn call_tool( &self, client: &LdkServerClient, name: &str, args: Value, ) -> ToolCallResult { @@ -67,94 +107,127 @@ impl ToolRegistry { } } +/// Mirrors the server's checks for whether `macaroon` may call `method`. +fn macaroon_allows(macaroon: &MacaroonInfo, method: &str) -> bool { + match method_authorization(method) { + // The server lets any valid macaroon inspect itself, even with a `method` caveat. + MethodAuthorization::AuthenticatedOnly => true, + MethodAuthorization::Unknown => false, + MethodAuthorization::Permission(required) => { + let has_permission = + macaroon.permissions.iter().any(|p| p == ADMIN_PERMISSION || p == required); + let method_allowed = macaroon + .caveats + .iter() + .all(|caveat| caveat.strip_prefix("method = ").is_none_or(|m| m == method)); + has_permission && method_allowed + }, + } +} + pub fn build_tool_registry() -> ToolRegistry { let tools = vec![ tool_spec( "get_permissions", + GET_PERMISSIONS_PATH, "Show the current macaroon's permissions and restrictions", schema::get_permissions_schema, |client, args| Box::pin(handlers::handle_get_permissions(client, args)), ), tool_spec( "revoke_macaroon", + REVOKE_MACAROON_PATH, "Revoke a macaroon for new requests", schema::revoke_macaroon_schema, |client, args| Box::pin(handlers::handle_revoke_macaroon(client, args)), ), tool_spec( "list_macaroons", + LIST_MACAROONS_PATH, "List macaroon IDs, names, permissions, and restrictions", schema::list_macaroons_schema, |client, args| Box::pin(handlers::handle_list_macaroons(client, args)), ), tool_spec( "create_macaroon", + CREATE_MACAROON_PATH, "Create a macaroon with chosen permissions and return its private token", schema::create_macaroon_schema, |client, args| Box::pin(handlers::handle_create_macaroon(client, args)), ), tool_spec( "get_node_info", + GET_NODE_INFO_PATH, "Retrieve node info including node_id, sync status, and best block", schema::get_node_info_schema, |client, args| Box::pin(handlers::handle_get_node_info(client, args)), ), tool_spec( "get_balances", + GET_BALANCES_PATH, "Retrieve an overview of all known balances (on-chain and Lightning)", schema::get_balances_schema, |client, args| Box::pin(handlers::handle_get_balances(client, args)), ), tool_spec( "onchain_receive", + ONCHAIN_RECEIVE_PATH, "Generate a new on-chain Bitcoin funding address", schema::onchain_receive_schema, |client, args| Box::pin(handlers::handle_onchain_receive(client, args)), ), tool_spec( "onchain_bump_fee", + ONCHAIN_BUMP_FEE_PATH, "Replace an unconfirmed outbound on-chain payment using RBF. Funding payments are not eligible. Returns the replacement transaction ID", schema::onchain_bump_fee_schema, |client, args| Box::pin(handlers::handle_onchain_bump_fee(client, args)), ), tool_spec( "onchain_send", + ONCHAIN_SEND_PATH, "Send an on-chain Bitcoin payment to an address", schema::onchain_send_schema, |client, args| Box::pin(handlers::handle_onchain_send(client, args)), ), tool_spec( "bolt11_receive", + BOLT11_RECEIVE_PATH, "Create a BOLT11 Lightning invoice to receive a payment", schema::bolt11_receive_schema, |client, args| Box::pin(handlers::handle_bolt11_receive(client, args)), ), tool_spec( "bolt11_receive_for_hash", + BOLT11_RECEIVE_FOR_HASH_PATH, "Create a BOLT11 Lightning invoice for a specific payment hash", schema::bolt11_receive_for_hash_schema, |client, args| Box::pin(handlers::handle_bolt11_receive_for_hash(client, args)), ), tool_spec( "bolt11_claim_for_id", + BOLT11_CLAIM_FOR_ID_PATH, "Manually claim a BOLT11 payment for a specific payment ID", schema::bolt11_claim_for_id_schema, |client, args| Box::pin(handlers::handle_bolt11_claim_for_id(client, args)), ), tool_spec( "bolt11_fail_for_id", + BOLT11_FAIL_FOR_ID_PATH, "Manually fail a BOLT11 payment for a specific payment ID", schema::bolt11_fail_for_id_schema, |client, args| Box::pin(handlers::handle_bolt11_fail_for_id(client, args)), ), tool_spec( "bolt11_receive_via_jit_channel", + BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, "Create a BOLT11 Lightning invoice to receive via an LSPS2 JIT channel", schema::bolt11_receive_via_jit_channel_schema, |client, args| Box::pin(handlers::handle_bolt11_receive_via_jit_channel(client, args)), ), tool_spec( "bolt11_receive_variable_amount_via_jit_channel", + BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, "Create a variable-amount BOLT11 Lightning invoice to receive via an LSPS2 JIT channel", schema::bolt11_receive_variable_amount_via_jit_channel_schema, |client, args| { @@ -165,12 +238,14 @@ pub fn build_tool_registry() -> ToolRegistry { ), tool_spec( "bolt11_receive_via_jit_channel_for_hash", + BOLT11_RECEIVE_VIA_JIT_CHANNEL_FOR_HASH_PATH, "Create a BOLT11 Lightning invoice to receive via an LSPS2 JIT channel for a given payment hash", schema::bolt11_receive_via_jit_channel_for_hash_schema, |client, args| Box::pin(handlers::handle_bolt11_receive_via_jit_channel_for_hash(client, args)), ), tool_spec( "bolt11_receive_variable_amount_via_jit_channel_for_hash", + BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_FOR_HASH_PATH, "Create a variable-amount BOLT11 Lightning invoice to receive via an LSPS2 JIT channel for a given payment hash", schema::bolt11_receive_variable_amount_via_jit_channel_for_hash_schema, |client, args| { @@ -181,222 +256,259 @@ pub fn build_tool_registry() -> ToolRegistry { ), tool_spec( "bolt11_send", + BOLT11_SEND_PATH, "Pay a BOLT11 Lightning invoice", schema::bolt11_send_schema, |client, args| Box::pin(handlers::handle_bolt11_send(client, args)), ), tool_spec( "bolt11_send_underpaying", + BOLT11_SEND_UNDERPAYING_PATH, "Send part of a fixed-amount BOLT11 invoice. Other nodes must send partial payments for the same invoice until the combined amount equals the invoice amount", schema::bolt11_send_underpaying_schema, |client, args| Box::pin(handlers::handle_bolt11_send_underpaying(client, args)), ), tool_spec( "bolt12_receive", + BOLT12_RECEIVE_PATH, "Create a reusable BOLT12 offer for receiving Lightning payments", schema::bolt12_receive_schema, |client, args| Box::pin(handlers::handle_bolt12_receive(client, args)), ), tool_spec( "bolt12_send", + BOLT12_SEND_PATH, "Pay a BOLT12 Lightning offer", schema::bolt12_send_schema, |client, args| Box::pin(handlers::handle_bolt12_send(client, args)), ), tool_spec( "bolt12_send_refund", + BOLT12_SEND_REFUND_PATH, "Create a BOLT12 refund that this node will pay", schema::bolt12_send_refund_schema, |client, args| Box::pin(handlers::handle_bolt12_send_refund(client, args)), ), tool_spec( "bolt12_receive_refund", + BOLT12_RECEIVE_REFUND_PATH, "Request an incoming payment for a BOLT12 refund", schema::bolt12_receive_refund_schema, |client, args| Box::pin(handlers::handle_bolt12_receive_refund(client, args)), ), tool_spec( "bolt12_create_payer_proof", + BOLT12_CREATE_PAYER_PROOF_PATH, "Create a BOLT12 payer proof for a payment this node made", schema::bolt12_create_payer_proof_schema, |client, args| Box::pin(handlers::handle_bolt12_create_payer_proof(client, args)), ), tool_spec( "spontaneous_send", + SPONTANEOUS_SEND_PATH, "Send a spontaneous (keysend) payment to a Lightning node", schema::spontaneous_send_schema, |client, args| Box::pin(handlers::handle_spontaneous_send(client, args)), ), tool_spec( "unified_send", + UNIFIED_SEND_PATH, "Send a payment given a BIP 21 URI or BIP 353 Human-Readable Name", schema::unified_send_schema, |client, args| Box::pin(handlers::handle_unified_send(client, args)), ), tool_spec( "open_channel", + OPEN_CHANNEL_PATH, "Open a new Lightning channel with a remote node", schema::open_channel_schema, |client, args| Box::pin(handlers::handle_open_channel(client, args)), ), tool_spec( "splice_in", + SPLICE_IN_PATH, "Increase a channel's balance by splicing in on-chain funds", schema::splice_in_schema, |client, args| Box::pin(handlers::handle_splice_in(client, args)), ), tool_spec( "splice_out", + SPLICE_OUT_PATH, "Decrease a channel's balance by splicing out to on-chain", schema::splice_out_schema, |client, args| Box::pin(handlers::handle_splice_out(client, args)), ), tool_spec( "bump_channel_funding_fee", + BUMP_CHANNEL_FUNDING_FEE_PATH, "Bump a pending splice fee, preserving its amount and destination. No general channel-opening fee bumping or caller-selected fee rate. Returns empty success when initiated", schema::bump_channel_funding_fee_schema, |client, args| Box::pin(handlers::handle_bump_channel_funding_fee(client, args)), ), tool_spec( "close_channel", + CLOSE_CHANNEL_PATH, "Cooperatively close a Lightning channel", schema::close_channel_schema, |client, args| Box::pin(handlers::handle_close_channel(client, args)), ), tool_spec( "force_close_channel", + FORCE_CLOSE_CHANNEL_PATH, "Force close a Lightning channel unilaterally", schema::force_close_channel_schema, |client, args| Box::pin(handlers::handle_force_close_channel(client, args)), ), tool_spec( "list_channels", + LIST_CHANNELS_PATH, "List all known Lightning channels", schema::list_channels_schema, |client, args| Box::pin(handlers::handle_list_channels(client, args)), ), tool_spec( "update_channel_config", + UPDATE_CHANNEL_CONFIG_PATH, "Update forwarding fees and CLTV delta for a channel", schema::update_channel_config_schema, |client, args| Box::pin(handlers::handle_update_channel_config(client, args)), ), tool_spec( "list_payments", + LIST_PAYMENTS_PATH, "List all payments (supports pagination via page_token)", schema::list_payments_schema, |client, args| Box::pin(handlers::handle_list_payments(client, args)), ), tool_spec( "get_payment_details", + GET_PAYMENT_DETAILS_PATH, "Get details of a specific payment by its ID", schema::get_payment_details_schema, |client, args| Box::pin(handlers::handle_get_payment_details(client, args)), ), tool_spec( "get_forwarded_payment_details", + GET_FORWARDED_PAYMENT_DETAILS_PATH, "Get a stored forwarded payment by its ID", schema::get_forwarded_payment_details_schema, |client, args| Box::pin(handlers::handle_get_forwarded_payment_details(client, args)), ), tool_spec( "get_forwarded_payment_tracking_mode", + GET_FORWARDED_PAYMENT_TRACKING_MODE_PATH, "Get the configured forwarding history tracking mode", schema::get_forwarded_payment_tracking_mode_schema, |client, args| Box::pin(handlers::handle_get_forwarded_payment_tracking_mode(client, args)), ), tool_spec( "get_channel_forwarding_stats", + GET_CHANNEL_FORWARDING_STATS_PATH, "Get forwarding statistics for a channel", schema::get_channel_forwarding_stats_schema, |client, args| Box::pin(handlers::handle_get_channel_forwarding_stats(client, args)), ), tool_spec( "list_channel_forwarding_stats", + LIST_CHANNEL_FORWARDING_STATS_PATH, "List channel forwarding statistics (paginated)", schema::list_channel_forwarding_stats_schema, |client, args| Box::pin(handlers::handle_list_channel_forwarding_stats(client, args)), ), tool_spec( "list_channel_pair_forwarding_stats", + LIST_CHANNEL_PAIR_FORWARDING_STATS_PATH, "List channel-pair forwarding statistics (paginated)", schema::list_channel_pair_forwarding_stats_schema, |client, args| Box::pin(handlers::handle_list_channel_pair_forwarding_stats(client, args)), ), tool_spec( "list_forwarded_payments", + LIST_FORWARDED_PAYMENTS_PATH, "Retrieve a paginated list of forwarded payments (use page_token for subsequent pages)", schema::list_forwarded_payments_schema, |client, args| Box::pin(handlers::handle_list_forwarded_payments(client, args)), ), tool_spec( "connect_peer", + CONNECT_PEER_PATH, "Connect to a Lightning peer without opening a channel", schema::connect_peer_schema, |client, args| Box::pin(handlers::handle_connect_peer(client, args)), ), tool_spec( "disconnect_peer", + DISCONNECT_PEER_PATH, "Disconnect from a Lightning peer", schema::disconnect_peer_schema, |client, args| Box::pin(handlers::handle_disconnect_peer(client, args)), ), tool_spec( "list_peers", + LIST_PEERS_PATH, "List all known Lightning peers", schema::list_peers_schema, |client, args| Box::pin(handlers::handle_list_peers(client, args)), ), tool_spec( "decode_invoice", + DECODE_INVOICE_PATH, "Decode a BOLT11 invoice and return its parsed fields", schema::decode_invoice_schema, |client, args| Box::pin(handlers::handle_decode_invoice(client, args)), ), tool_spec( "decode_offer", + DECODE_OFFER_PATH, "Decode a BOLT12 offer and return its parsed fields", schema::decode_offer_schema, |client, args| Box::pin(handlers::handle_decode_offer(client, args)), ), tool_spec( "sign_message", + SIGN_MESSAGE_PATH, "Sign a message with the node's secret key", schema::sign_message_schema, |client, args| Box::pin(handlers::handle_sign_message(client, args)), ), tool_spec( "verify_signature", + VERIFY_SIGNATURE_PATH, "Verify a signature against a message and public key", schema::verify_signature_schema, |client, args| Box::pin(handlers::handle_verify_signature(client, args)), ), tool_spec( "export_pathfinding_scores", + EXPORT_PATHFINDING_SCORES_PATH, "Export the pathfinding scores used by the Lightning router", schema::export_pathfinding_scores_schema, |client, args| Box::pin(handlers::handle_export_pathfinding_scores(client, args)), ), tool_spec( "graph_list_channels", + GRAPH_LIST_CHANNELS_PATH, "List all known short channel IDs in the network graph", schema::graph_list_channels_schema, |client, args| Box::pin(handlers::handle_graph_list_channels(client, args)), ), tool_spec( "graph_get_channel", + GRAPH_GET_CHANNEL_PATH, "Get channel information from the network graph by short channel ID", schema::graph_get_channel_schema, |client, args| Box::pin(handlers::handle_graph_get_channel(client, args)), ), tool_spec( "graph_list_nodes", + GRAPH_LIST_NODES_PATH, "List all known node IDs in the network graph", schema::graph_list_nodes_schema, |client, args| Box::pin(handlers::handle_graph_list_nodes(client, args)), ), tool_spec( "graph_get_node", + GRAPH_GET_NODE_PATH, "Get node information from the network graph by node ID", schema::graph_get_node_schema, |client, args| Box::pin(handlers::handle_graph_get_node(client, args)), @@ -404,6 +516,7 @@ pub fn build_tool_registry() -> ToolRegistry { ]; let mut definitions = Vec::with_capacity(tools.len()); + let mut rpc_methods = HashMap::with_capacity(tools.len()); let mut handlers = HashMap::with_capacity(tools.len()); for spec in tools { definitions.push(ToolDefinition { @@ -411,8 +524,118 @@ pub fn build_tool_registry() -> ToolRegistry { description: spec.description.to_string(), input_schema: (spec.input_schema)(), }); + rpc_methods.insert(spec.name, spec.rpc_method); handlers.insert(spec.name, spec.handler); } - ToolRegistry { definitions, handlers } + ToolRegistry { definitions, rpc_methods, handlers } +} + +#[cfg(test)] +mod tests { + use ldk_server_client::ldk_server_grpc::permissions::{ + INVOICE_PERMISSIONS, NODE_READ_PERMISSION, READONLY_PERMISSIONS, + }; + + use super::*; + + fn macaroon(permissions: &[&str], caveats: &[&str]) -> MacaroonInfo { + MacaroonInfo { + id: "id".to_string(), + name: "test".to_string(), + permissions: permissions.iter().map(|p| p.to_string()).collect(), + caveats: caveats.iter().map(|c| c.to_string()).collect(), + } + } + + fn visible_tools(macaroon: &MacaroonInfo) -> Vec { + let mut registry = build_tool_registry(); + registry.retain_allowed_tools(macaroon); + let mut names: Vec<_> = registry.list_tools().iter().map(|t| t.name.clone()).collect(); + names.sort(); + names + } + + fn tools_requiring(permissions: &[&str]) -> Vec { + let registry = build_tool_registry(); + let mut names: Vec<_> = registry + .list_tools() + .iter() + .map(|tool| tool.name.clone()) + .filter(|name| match method_authorization(registry.rpc_methods[name.as_str()]) { + MethodAuthorization::Permission(required) => permissions.contains(&required), + MethodAuthorization::AuthenticatedOnly => true, + MethodAuthorization::Unknown => false, + }) + .collect(); + names.sort(); + names + } + + #[test] + fn every_tool_has_a_known_rpc_method() { + let registry = build_tool_registry(); + assert_eq!(registry.rpc_methods.len(), registry.list_tools().len()); + for tool in registry.list_tools() { + let method = registry.rpc_methods[tool.name.as_str()]; + assert_ne!( + method_authorization(method), + MethodAuthorization::Unknown, + "Tool {} calls RPC {method}, which has no permission mapping", + tool.name + ); + } + assert_eq!(registry.rpc_methods["get_permissions"], GET_PERMISSIONS_PATH); + assert_eq!(registry.rpc_methods["bolt11_receive_for_hash"], BOLT11_RECEIVE_FOR_HASH_PATH); + } + + #[test] + fn admin_macaroon_sees_every_tool() { + let mut expected: Vec<_> = + build_tool_registry().list_tools().iter().map(|t| t.name.clone()).collect(); + expected.sort(); + assert_eq!(visible_tools(&macaroon(&[ADMIN_PERMISSION], &[])), expected); + let expiring = macaroon(&[ADMIN_PERMISSION], &["time-before = 4102444800"]); + assert_eq!(visible_tools(&expiring), expected); + } + + #[test] + fn restricted_macaroon_sees_only_permitted_tools() { + assert_eq!(visible_tools(&macaroon(&[], &[])), ["get_permissions"]); + let reader = visible_tools(&macaroon(&[NODE_READ_PERMISSION], &[])); + assert_eq!( + reader, + ["export_pathfinding_scores", "get_balances", "get_node_info", "get_permissions"] + ); + assert_eq!( + visible_tools(&macaroon(&READONLY_PERMISSIONS, &[])), + tools_requiring(&READONLY_PERMISSIONS) + ); + let invoice = visible_tools(&macaroon(&INVOICE_PERMISSIONS, &[])); + assert_eq!(invoice, tools_requiring(&INVOICE_PERMISSIONS)); + assert!(invoice.contains(&"bolt11_receive".to_string())); + for hidden in ["bolt11_send", "onchain_send", "open_channel", "create_macaroon"] { + assert!(!invoice.contains(&hidden.to_string()), "{hidden} should be hidden"); + } + } + + #[test] + fn method_caveats_limit_visible_tools() { + let limited = macaroon(&[ADMIN_PERMISSION], &["method = GetBalances"]); + assert_eq!(visible_tools(&limited), ["get_balances", "get_permissions"]); + // The method must also be covered by the granted permissions. + let unpermitted = macaroon(&[NODE_READ_PERMISSION], &["method = ListChannels"]); + assert_eq!(visible_tools(&unpermitted), ["get_permissions"]); + let conflicting = + macaroon(&[ADMIN_PERMISSION], &["method = GetBalances", "method = GetNodeInfo"]); + assert_eq!(visible_tools(&conflicting), ["get_permissions"]); + } + + #[test] + fn hidden_tools_remain_callable() { + let mut registry = build_tool_registry(); + registry.retain_allowed_tools(&macaroon(&[NODE_READ_PERMISSION], &[])); + assert!(registry.list_tools().iter().all(|tool| tool.name != "bolt11_send")); + assert!(registry.handlers.contains_key("bolt11_send")); + } } diff --git a/ldk-server/src/macaroons/authorization.rs b/ldk-server/src/macaroons/authorization.rs index 35be51c4..f8e918ec 100644 --- a/ldk-server/src/macaroons/authorization.rs +++ b/ldk-server/src/macaroons/authorization.rs @@ -9,115 +9,13 @@ //! RPC permission requirements. -use ldk_server_grpc::endpoints::{ - BOLT11_CLAIM_FOR_ID_PATH, BOLT11_FAIL_FOR_ID_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, - BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_FOR_HASH_PATH, - BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, - BOLT11_RECEIVE_VIA_JIT_CHANNEL_FOR_HASH_PATH, BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, - BOLT11_SEND_PATH, BOLT11_SEND_UNDERPAYING_PATH, BOLT12_CREATE_PAYER_PROOF_PATH, - BOLT12_RECEIVE_PATH, BOLT12_RECEIVE_REFUND_PATH, BOLT12_SEND_PATH, BOLT12_SEND_REFUND_PATH, - BUMP_CHANNEL_FUNDING_FEE_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, CREATE_MACAROON_PATH, - DECODE_INVOICE_PATH, DECODE_OFFER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, - FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_CHANNEL_FORWARDING_STATS_PATH, - GET_FORWARDED_PAYMENT_DETAILS_PATH, GET_FORWARDED_PAYMENT_TRACKING_MODE_PATH, - GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, GET_PERMISSIONS_PATH, GRAPH_GET_CHANNEL_PATH, - GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, LIST_CHANNELS_PATH, - LIST_CHANNEL_FORWARDING_STATS_PATH, LIST_CHANNEL_PAIR_FORWARDING_STATS_PATH, - LIST_FORWARDED_PAYMENTS_PATH, LIST_MACAROONS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, - ONCHAIN_BUMP_FEE_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, - REVOKE_MACAROON_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH, - SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, - VERIFY_SIGNATURE_PATH, -}; -use ldk_server_grpc::permissions::{ - CHANNELS_FORCE_CLOSE_PERMISSION, CHANNELS_MANAGE_PERMISSION, CHANNELS_READ_PERMISSION, - EVENTS_READ_PERMISSION, GRAPH_READ_PERMISSION, INVOICES_CREATE_PERMISSION, - MACAROONS_MANAGE_PERMISSION, MESSAGES_SIGN_PERMISSION, MESSAGES_VERIFY_PERMISSION, - NODE_READ_PERMISSION, ONCHAIN_RECEIVE_PERMISSION, ONCHAIN_SEND_PERMISSION, - PAYMENTS_CLAIM_PERMISSION, PAYMENTS_READ_PERMISSION, PAYMENTS_SEND_PERMISSION, - PEERS_MANAGE_PERMISSION, PEERS_READ_PERMISSION, UTILITIES_READ_PERMISSION, -}; - -pub(crate) enum MethodAuthorization { - Permission(&'static str), - AuthenticatedOnly, - Unknown, -} - -pub(crate) fn method_authorization(method: &str) -> MethodAuthorization { - match method { - GET_NODE_INFO_PATH | GET_BALANCES_PATH | EXPORT_PATHFINDING_SCORES_PATH => { - MethodAuthorization::Permission(NODE_READ_PERMISSION) - }, - ONCHAIN_RECEIVE_PATH => MethodAuthorization::Permission(ONCHAIN_RECEIVE_PERMISSION), - ONCHAIN_SEND_PATH | ONCHAIN_BUMP_FEE_PATH => { - MethodAuthorization::Permission(ONCHAIN_SEND_PERMISSION) - }, - BOLT11_RECEIVE_PATH - | BOLT11_RECEIVE_FOR_HASH_PATH - | BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH - | BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH - | BOLT11_RECEIVE_VIA_JIT_CHANNEL_FOR_HASH_PATH - | BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_FOR_HASH_PATH - | BOLT12_RECEIVE_PATH - | BOLT12_RECEIVE_REFUND_PATH => MethodAuthorization::Permission(INVOICES_CREATE_PERMISSION), - BOLT11_CLAIM_FOR_ID_PATH | BOLT11_FAIL_FOR_ID_PATH => { - MethodAuthorization::Permission(PAYMENTS_CLAIM_PERMISSION) - }, - BOLT11_SEND_PATH - | BOLT11_SEND_UNDERPAYING_PATH - | BOLT12_SEND_PATH - | BOLT12_SEND_REFUND_PATH - | SPONTANEOUS_SEND_PATH - | UNIFIED_SEND_PATH - | SPLICE_OUT_PATH => MethodAuthorization::Permission(PAYMENTS_SEND_PERMISSION), - GET_PAYMENT_DETAILS_PATH - | LIST_PAYMENTS_PATH - | LIST_FORWARDED_PAYMENTS_PATH - | GET_FORWARDED_PAYMENT_DETAILS_PATH - | GET_FORWARDED_PAYMENT_TRACKING_MODE_PATH - | GET_CHANNEL_FORWARDING_STATS_PATH - | LIST_CHANNEL_FORWARDING_STATS_PATH - | LIST_CHANNEL_PAIR_FORWARDING_STATS_PATH => { - MethodAuthorization::Permission(PAYMENTS_READ_PERMISSION) - }, - LIST_CHANNELS_PATH => MethodAuthorization::Permission(CHANNELS_READ_PERMISSION), - OPEN_CHANNEL_PATH - | UPDATE_CHANNEL_CONFIG_PATH - | CLOSE_CHANNEL_PATH - | SPLICE_IN_PATH - | BUMP_CHANNEL_FUNDING_FEE_PATH => MethodAuthorization::Permission(CHANNELS_MANAGE_PERMISSION), - FORCE_CLOSE_CHANNEL_PATH => { - MethodAuthorization::Permission(CHANNELS_FORCE_CLOSE_PERMISSION) - }, - LIST_PEERS_PATH => MethodAuthorization::Permission(PEERS_READ_PERMISSION), - CONNECT_PEER_PATH | DISCONNECT_PEER_PATH => { - MethodAuthorization::Permission(PEERS_MANAGE_PERMISSION) - }, - SIGN_MESSAGE_PATH | BOLT12_CREATE_PAYER_PROOF_PATH => { - MethodAuthorization::Permission(MESSAGES_SIGN_PERMISSION) - }, - VERIFY_SIGNATURE_PATH => MethodAuthorization::Permission(MESSAGES_VERIFY_PERMISSION), - GRAPH_LIST_CHANNELS_PATH - | GRAPH_GET_CHANNEL_PATH - | GRAPH_LIST_NODES_PATH - | GRAPH_GET_NODE_PATH => MethodAuthorization::Permission(GRAPH_READ_PERMISSION), - DECODE_INVOICE_PATH | DECODE_OFFER_PATH => { - MethodAuthorization::Permission(UTILITIES_READ_PERMISSION) - }, - SUBSCRIBE_EVENTS_PATH => MethodAuthorization::Permission(EVENTS_READ_PERMISSION), - CREATE_MACAROON_PATH | LIST_MACAROONS_PATH | REVOKE_MACAROON_PATH => { - MethodAuthorization::Permission(MACAROONS_MANAGE_PERMISSION) - }, - GET_PERMISSIONS_PATH => MethodAuthorization::AuthenticatedOnly, - _ => MethodAuthorization::Unknown, - } -} +pub(crate) use ldk_server_grpc::permissions::{method_authorization, MethodAuthorization}; #[cfg(test)] mod tests { use std::collections::BTreeSet; + use ldk_server_grpc::endpoints::GET_PERMISSIONS_PATH; use ldk_server_grpc::permissions::ALL_PERMISSIONS; use super::*;