From 167a1c35f9a6c4e177740e0eafaa49b8ebf713b9 Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Thu, 14 Nov 2024 01:58:28 +0300 Subject: [PATCH] Initial bitwise RPC authorization-permission system --- consensus/core/src/config/mod.rs | 4 +++ rpc/core/src/api/ops.rs | 4 +++ rpc/core/src/error.rs | 3 ++ rpc/macros/src/core/mod.rs | 1 + rpc/macros/src/core/service.rs | 19 +++++++++++++ rpc/macros/src/lib.rs | 6 ++++ rpc/service/Cargo.toml | 1 + rpc/service/src/flags.rs | 44 ++++++++++++++++++++++++++++ rpc/service/src/lib.rs | 1 + rpc/service/src/service.rs | 49 +++++++++++++++++++++++++++++++- 10 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 rpc/macros/src/core/mod.rs create mode 100644 rpc/macros/src/core/service.rs create mode 100644 rpc/service/src/flags.rs diff --git a/consensus/core/src/config/mod.rs b/consensus/core/src/config/mod.rs index d62bf15a94..6296d376f0 100644 --- a/consensus/core/src/config/mod.rs +++ b/consensus/core/src/config/mod.rs @@ -68,6 +68,9 @@ pub struct Config { /// A scale factor to apply to memory allocation bounds pub ram_scale: f64, + + /// Bitwise flag for configuring allowed RPC calls + pub rpc_flags: u128, } impl Config { @@ -95,6 +98,7 @@ impl Config { initial_utxo_set: Default::default(), disable_upnp: false, ram_scale: 1.0, + rpc_flags: u128::MAX, } } diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index 26ca356eb0..9cae7d4943 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -139,6 +139,10 @@ pub enum RpcApiOps { } impl RpcApiOps { + pub fn bitmask(&self) -> u128 { + 1 << (*self as u128 - 110) // Only applies for RPC methods -- means it covers all calls up to 237. + } + pub fn is_subscription(&self) -> bool { matches!( self, diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index 0e2bfee225..adbdff9b1c 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -60,6 +60,9 @@ pub enum RpcError { #[error("Transaction {0} not found")] TransactionNotFound(TransactionId), + #[error("Method unavailable. Disabled through RPC flags.")] + UnauthorizedMethod, + #[error("Method unavailable. Run the node with the --utxoindex argument.")] NoUtxoIndex, diff --git a/rpc/macros/src/core/mod.rs b/rpc/macros/src/core/mod.rs new file mode 100644 index 0000000000..1f278a4d51 --- /dev/null +++ b/rpc/macros/src/core/mod.rs @@ -0,0 +1 @@ +pub mod service; diff --git a/rpc/macros/src/core/service.rs b/rpc/macros/src/core/service.rs new file mode 100644 index 0000000000..a46f31ac3a --- /dev/null +++ b/rpc/macros/src/core/service.rs @@ -0,0 +1,19 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemFn, Path}; + +pub fn auth(attr: TokenStream, item: TokenStream) -> TokenStream { + let api_op = parse_macro_input!(attr as Path); + let mut func = parse_macro_input!(item as ItemFn); + + let check = syn::parse2(quote! { + if !self.flags.has_enabled(#api_op) { + // As macro processing happens after async_trait processing its wrapped with async_trait return type + return std::boxed::Box::pin(std::future::ready(Err(RpcError::UnauthorizedMethod))); + } + }) + .unwrap(); + + func.block.stmts.insert(0, check); + quote!(#func).into() +} diff --git a/rpc/macros/src/lib.rs b/rpc/macros/src/lib.rs index 9ca49bf54a..dbbfe37b88 100644 --- a/rpc/macros/src/lib.rs +++ b/rpc/macros/src/lib.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; +mod core; mod grpc; mod handler; mod wrpc; @@ -45,3 +46,8 @@ pub fn build_grpc_server_interface(input: TokenStream) -> TokenStream { pub fn test_wrpc_serializer(input: TokenStream) -> TokenStream { wrpc::test::build_test(input) } + +#[proc_macro_attribute] +pub fn auth(attr: TokenStream, item: TokenStream) -> TokenStream { + core::service::auth(attr, item) +} diff --git a/rpc/service/Cargo.toml b/rpc/service/Cargo.toml index 54e9764088..ce68439a2a 100644 --- a/rpc/service/Cargo.toml +++ b/rpc/service/Cargo.toml @@ -24,6 +24,7 @@ kaspa-p2p-flows.workspace = true kaspa-p2p-lib.workspace = true kaspa-perf-monitor.workspace = true kaspa-rpc-core.workspace = true +kaspa-rpc-macros.workspace = true kaspa-txscript.workspace = true kaspa-utils.workspace = true kaspa-utils-tower.workspace = true diff --git a/rpc/service/src/flags.rs b/rpc/service/src/flags.rs new file mode 100644 index 0000000000..6d44dd0ff3 --- /dev/null +++ b/rpc/service/src/flags.rs @@ -0,0 +1,44 @@ +use kaspa_rpc_core::api::ops::RpcApiOps; + +// Struct to manage flags as a combined bitmask +#[derive(Debug)] +pub struct Flags { + bitmask: u128, +} + +impl Flags { + // Create an empty flag set + pub fn new() -> Self { + Flags { bitmask: 0 } + } + + // Adds a flag + pub fn add(&mut self, op: RpcApiOps) { + self.bitmask |= op.bitmask(); + } + + // Removes a flag + pub fn remove(&mut self, op: RpcApiOps) { + self.bitmask &= !op.bitmask(); + } + + // Check if a flag is enabled + pub fn has_enabled(&self, op: RpcApiOps) -> bool { + (self.bitmask & op.bitmask()) != 0 + } + + // Create a flag set from a slice of operations + pub fn from_ops(ops: &[RpcApiOps]) -> Self { + let mut permissions = Flags::new(); + for &op in ops { + permissions.add(op); + } + permissions + } +} + +impl From for Flags { + fn from(bitmask: u128) -> Self { + Flags { bitmask } + } +} diff --git a/rpc/service/src/lib.rs b/rpc/service/src/lib.rs index 6d5e825765..54beb17665 100644 --- a/rpc/service/src/lib.rs +++ b/rpc/service/src/lib.rs @@ -1,3 +1,4 @@ pub mod collector; pub mod converter; +pub mod flags; pub mod service; diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index d75ff770b0..ffbc93cc9b 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -3,6 +3,7 @@ use super::collector::{CollectorFromConsensus, CollectorFromIndex}; use crate::converter::feerate_estimate::{FeeEstimateConverter, FeeEstimateVerboseConverter}; use crate::converter::{consensus::ConsensusConverter, index::IndexConverter, protocol::ProtocolConverter}; +use crate::flags::Flags; use crate::service::NetworkType::{Mainnet, Testnet}; use async_trait::async_trait; use kaspa_consensus_core::api::counters::ProcessingCounters; @@ -53,6 +54,7 @@ use kaspa_notify::{ use kaspa_p2p_flows::flow_context::FlowContext; use kaspa_p2p_lib::common::ProtocolError; use kaspa_perf_monitor::{counters::CountersSnapshot, Monitor as PerfMonitor}; +use kaspa_rpc_core::api::ops::RpcApiOps; use kaspa_rpc_core::{ api::{ connection::DynRpcConnection, @@ -63,6 +65,7 @@ use kaspa_rpc_core::{ notify::connection::ChannelConnection, Notification, RpcError, RpcResult, }; +use kaspa_rpc_macros::auth; use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script}; use kaspa_utils::expiring_cache::ExpiringCache; use kaspa_utils::sysinfo::SystemInfo; @@ -118,6 +121,7 @@ pub struct RpcCoreService { system_info: SystemInfo, fee_estimate_cache: ExpiringCache, fee_estimate_verbose_cache: ExpiringCache>, + flags: Flags, } const RPC_CORE: &str = "rpc-core"; @@ -195,10 +199,12 @@ impl RpcCoreService { // Protocol converter let protocol_converter = Arc::new(ProtocolConverter::new(flow_context.clone())); - // Create the rcp-core notifier + // Create the rpc-core notifier let notifier = Arc::new(Notifier::new(RPC_CORE, EVENT_TYPE_ARRAY[..].into(), collectors, subscribers, subscription_context, 1, policies)); + let flags: Flags = config.rpc_flags.into(); + Self { consensus_manager, notifier, @@ -221,6 +227,7 @@ impl RpcCoreService { system_info, fee_estimate_cache: ExpiringCache::new(Duration::from_millis(500), Duration::from_millis(1000)), fee_estimate_verbose_cache: ExpiringCache::new(Duration::from_millis(500), Duration::from_millis(1000)), + flags, } } @@ -288,6 +295,7 @@ impl RpcCoreService { #[async_trait] impl RpcApi for RpcCoreService { + #[auth(RpcApiOps::SubmitBlock)] async fn submit_block_call( &self, _connection: Option<&DynRpcConnection>, @@ -350,6 +358,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[auth(RpcApiOps::GetBlockTemplate)] async fn get_block_template_call( &self, _connection: Option<&DynRpcConnection>, @@ -386,6 +395,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[auth(RpcApiOps::GetCurrentBlockColor)] async fn get_current_block_color_call( &self, _connection: Option<&DynRpcConnection>, @@ -399,6 +409,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[auth(RpcApiOps::GetBlock)] async fn get_block_call(&self, _connection: Option<&DynRpcConnection>, request: GetBlockRequest) -> RpcResult { // TODO: test let session = self.consensus_manager.consensus().session().await; @@ -411,6 +422,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[auth(RpcApiOps::GetBlocks)] async fn get_blocks_call( &self, _connection: Option<&DynRpcConnection>, @@ -464,6 +476,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBlocksResponse { block_hashes, blocks }) } + #[auth(RpcApiOps::GetInfo)] async fn get_info_call(&self, _connection: Option<&DynRpcConnection>, _request: GetInfoRequest) -> RpcResult { let is_nearly_synced = self.consensus_manager.consensus().unguarded_session().async_is_nearly_synced().await; Ok(GetInfoResponse { @@ -477,6 +490,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[auth(RpcApiOps::GetMempoolEntry)] async fn get_mempool_entry_call( &self, _connection: Option<&DynRpcConnection>, @@ -490,6 +504,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntryResponse::new(self.consensus_converter.get_mempool_entry(&session, &transaction))) } + #[auth(RpcApiOps::GetMempoolEntries)] async fn get_mempool_entries_call( &self, _connection: Option<&DynRpcConnection>, @@ -506,6 +521,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntriesResponse::new(mempool_entries)) } + #[auth(RpcApiOps::GetMempoolEntriesByAddresses)] async fn get_mempool_entries_by_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -532,6 +548,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetMempoolEntriesByAddressesResponse::new(mempool_entries)) } + #[auth(RpcApiOps::SubmitTransaction)] async fn submit_transaction_call( &self, _connection: Option<&DynRpcConnection>, @@ -557,6 +574,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(SubmitTransactionResponse::new(transaction_id)) } + #[auth(RpcApiOps::SubmitTransactionReplacement)] async fn submit_transaction_replacement_call( &self, _connection: Option<&DynRpcConnection>, @@ -574,6 +592,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(SubmitTransactionReplacementResponse::new(transaction_id, (&*replaced_transaction).into())) } + #[auth(RpcApiOps::GetCurrentNetwork)] async fn get_current_network_call( &self, _connection: Option<&DynRpcConnection>, @@ -582,6 +601,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetCurrentNetworkResponse::new(*self.config.net)) } + #[auth(RpcApiOps::GetSubnetwork)] async fn get_subnetwork_call( &self, _connection: Option<&DynRpcConnection>, @@ -590,10 +610,12 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } + #[auth(RpcApiOps::GetSink)] async fn get_sink_call(&self, _connection: Option<&DynRpcConnection>, _: GetSinkRequest) -> RpcResult { Ok(GetSinkResponse::new(self.consensus_manager.consensus().unguarded_session().async_get_sink().await)) } + #[auth(RpcApiOps::GetSinkBlueScore)] async fn get_sink_blue_score_call( &self, _connection: Option<&DynRpcConnection>, @@ -603,6 +625,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetSinkBlueScoreResponse::new(session.async_get_ghostdag_data(session.async_get_sink().await).await?.blue_score)) } + #[auth(RpcApiOps::GetVirtualChainFromBlock)] async fn get_virtual_chain_from_block_call( &self, _connection: Option<&DynRpcConnection>, @@ -631,6 +654,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetVirtualChainFromBlockResponse::new(virtual_chain_batch.removed, virtual_chain_batch.added, accepted_transaction_ids)) } + #[auth(RpcApiOps::GetBlockCount)] async fn get_block_count_call( &self, _connection: Option<&DynRpcConnection>, @@ -639,6 +663,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(self.consensus_manager.consensus().unguarded_session().async_estimate_block_count().await) } + #[auth(RpcApiOps::GetUtxosByAddresses)] async fn get_utxos_by_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -653,6 +678,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetUtxosByAddressesResponse::new(self.index_converter.get_utxos_by_addresses_entries(&entry_map))) } + #[auth(RpcApiOps::GetBalanceByAddress)] async fn get_balance_by_address_call( &self, _connection: Option<&DynRpcConnection>, @@ -666,6 +692,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBalanceByAddressResponse::new(balance)) } + #[auth(RpcApiOps::GetBalancesByAddresses)] async fn get_balances_by_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -687,6 +714,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetBalancesByAddressesResponse::new(entries)) } + #[auth(RpcApiOps::GetCoinSupply)] async fn get_coin_supply_call( &self, _connection: Option<&DynRpcConnection>, @@ -700,6 +728,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetCoinSupplyResponse::new(MAX_SOMPI, circulating_sompi)) } + #[auth(RpcApiOps::GetDaaScoreTimestampEstimate)] async fn get_daa_score_timestamp_estimate_call( &self, _connection: Option<&DynRpcConnection>, @@ -759,6 +788,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetDaaScoreTimestampEstimateResponse::new(timestamps)) } + #[auth(RpcApiOps::GetFeeEstimate)] async fn get_fee_estimate_call( &self, _connection: Option<&DynRpcConnection>, @@ -770,6 +800,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetFeeEstimateResponse { estimate }) } + #[auth(RpcApiOps::GetFeeEstimateExperimental)] async fn get_fee_estimate_experimental_call( &self, connection: Option<&DynRpcConnection>, @@ -794,10 +825,12 @@ NOTE: This error usually indicates an RPC conversion error between the node and } } + #[auth(RpcApiOps::Ping)] async fn ping_call(&self, _connection: Option<&DynRpcConnection>, _: PingRequest) -> RpcResult { Ok(PingResponse {}) } + #[auth(RpcApiOps::GetHeaders)] async fn get_headers_call( &self, _connection: Option<&DynRpcConnection>, @@ -806,6 +839,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } + #[auth(RpcApiOps::GetBlockDagInfo)] async fn get_block_dag_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -828,6 +862,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and )) } + #[auth(RpcApiOps::EstimateNetworkHashesPerSecond)] async fn estimate_network_hashes_per_second_call( &self, _connection: Option<&DynRpcConnection>, @@ -860,6 +895,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and )) } + #[auth(RpcApiOps::AddPeer)] async fn add_peer_call(&self, _connection: Option<&DynRpcConnection>, request: AddPeerRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("AddPeer RPC command called while node in safe RPC mode -- ignoring."); @@ -874,6 +910,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(AddPeerResponse {}) } + #[auth(RpcApiOps::GetPeerAddresses)] async fn get_peer_addresses_call( &self, _connection: Option<&DynRpcConnection>, @@ -883,6 +920,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetPeerAddressesResponse::new(address_manager.get_all_addresses(), address_manager.get_all_banned_addresses())) } + #[auth(RpcApiOps::Ban)] async fn ban_call(&self, _connection: Option<&DynRpcConnection>, request: BanRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("Ban RPC command called while node in safe RPC mode -- ignoring."); @@ -900,6 +938,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(BanResponse {}) } + #[auth(RpcApiOps::Unban)] async fn unban_call(&self, _connection: Option<&DynRpcConnection>, request: UnbanRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("Unban RPC command called while node in safe RPC mode -- ignoring."); @@ -914,6 +953,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(UnbanResponse {}) } + #[auth(RpcApiOps::GetConnectedPeerInfo)] async fn get_connected_peer_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -924,6 +964,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetConnectedPeerInfoResponse::new(peer_info)) } + #[auth(RpcApiOps::Shutdown)] async fn shutdown_call(&self, _connection: Option<&DynRpcConnection>, _: ShutdownRequest) -> RpcResult { if !self.config.unsafe_rpc { warn!("Shutdown RPC command called while node in safe RPC mode -- ignoring."); @@ -945,6 +986,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(ShutdownResponse {}) } + #[auth(RpcApiOps::ResolveFinalityConflict)] async fn resolve_finality_conflict_call( &self, _connection: Option<&DynRpcConnection>, @@ -957,6 +999,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Err(RpcError::NotImplemented) } + #[auth(RpcApiOps::GetConnections)] async fn get_connections_call( &self, _connection: Option<&DynRpcConnection>, @@ -975,6 +1018,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(GetConnectionsResponse { clients, peers, profile_data }) } + #[auth(RpcApiOps::GetMetrics)] async fn get_metrics_call(&self, _connection: Option<&DynRpcConnection>, req: GetMetricsRequest) -> RpcResult { let CountersSnapshot { resident_set_size, @@ -1068,6 +1112,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(response) } + #[auth(RpcApiOps::GetSystemInfo)] async fn get_system_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -1086,6 +1131,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and Ok(response) } + #[auth(RpcApiOps::GetServerInfo)] async fn get_server_info_call( &self, _connection: Option<&DynRpcConnection>, @@ -1106,6 +1152,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and }) } + #[auth(RpcApiOps::GetSyncStatus)] async fn get_sync_status_call( &self, _connection: Option<&DynRpcConnection>,