diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 90885b02db..f2cffb81d3 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -376,3 +376,20 @@ void StopHTTPRPC() httpRPCTimerInterface.reset(); } } + +std::set GetWhitelistedRpcs(const std::string& user_name) +{ + if (auto it = g_rpc_whitelist.find(user_name); it != g_rpc_whitelist.end()) { + return it->second; + } + if (g_rpc_whitelist_default) { + return std::set(); + } + + // Build a list of every method + std::set allowed_methods; + for (const auto& method_name : tableRPC.listCommands()) { + allowed_methods.insert(method_name); + } + return allowed_methods; +} diff --git a/src/httprpc.h b/src/httprpc.h index 25c1a60c90..7120cd7ed4 100644 --- a/src/httprpc.h +++ b/src/httprpc.h @@ -6,6 +6,8 @@ #define BITCOIN_HTTPRPC_H #include +#include +#include #include /** Start HTTP RPC subsystem. @@ -37,4 +39,8 @@ void InterruptREST(); */ void StopREST(); +/** Returns a collection of whitelisted RPCs for the given user + */ +std::set GetWhitelistedRpcs(const std::string& user_name); + #endif // BITCOIN_HTTPRPC_H diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index e5caa6d3d9..e9a440f14d 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -258,9 +259,43 @@ static RPCHelpMan getrpcinfo() }; } +static RPCHelpMan getrpcwhitelist() +{ + return RPCHelpMan{"getrpcwhitelist", + "\nReturns whitelisted RPCs for the current user.\n", + {}, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::OBJ_DYN, "methods", "List of RPCs that the user is allowed to call", + { + {RPCResult::Type::NONE, "rpc", "Key is name of RPC method, value is null"}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("getrpcwhitelist", "") + + HelpExampleRpc("getrpcwhitelist", "")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + UniValue whitelisted_rpcs(UniValue::VOBJ); + const std::set& whitelist = GetWhitelistedRpcs(request.authUser); + for (const auto& rpc : whitelist) { + whitelisted_rpcs.pushKV(rpc, NullUniValue); + } + + UniValue result(UniValue::VOBJ); + result.pushKV("methods", whitelisted_rpcs); + + return result; +} + }; +} + static const CRPCCommand vRPCCommands[]{ /* Overall control/query calls */ {"control", &getrpcinfo}, + {"control", &getrpcwhitelist}, {"control", &help}, {"control", &stop}, {"control", &uptime}, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index f8eb3c5857..7e03c3dd59 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -151,6 +151,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getrawmempool", "getrawtransaction", "getrpcinfo", + "getrpcwhitelist", "gettxout", "gettxoutsetinfo", "gettxspendingprevout", diff --git a/test/functional/rpc_getrpcwhitelist.py b/test/functional/rpc_getrpcwhitelist.py new file mode 100755 index 0000000000..41e6b45bca --- /dev/null +++ b/test/functional/rpc_getrpcwhitelist.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Test getrpcwhitelist RPC call. +""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + get_datadir_path, + str_to_b64str +) +import http.client +import json +import os +import urllib.parse + +def call_rpc(node, settings, rpc): + url = urllib.parse.urlparse(node.url) + headers = {"Authorization": "Basic " + str_to_b64str('{}:{}'.format(settings[0], settings[2]))} + conn = http.client.HTTPConnection(url.hostname, url.port) + conn.connect() + conn.request('POST', '/', '{"method": "' + rpc + '"}', headers) + resp = conn.getresponse() + code = resp.status + if code == 200: + json_ret = json.loads(resp.read().decode()) + else: + json_ret = {"result": None} + conn.close() + return {"status": code, "json": json_ret['result']} + +class RPCWhitelistTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def setup_chain(self): + super().setup_chain() + self.settings = ["dummy", + "4e799db4b65924f4468b1c9ff3a68109$5fcd282dcaf4ae74599934a543626c0a11e7e83ead30f07b182058ead8e85da9", + "dummypwd", + "getbalance,getrpcwhitelist,getwalletinfo"] + self.settings_forbidden = ["dummy2", + "f3d319f64b076012f75626c9d895fced$7f55381a24fda02c5de7c18fc377f56fc573149b4d6f83daa9fd584210b51f99", + "dummy2pwd", + "getbalance,getwalletinfo"] + + # declare rpc-whitelisting entries + with open(os.path.join(get_datadir_path(self.options.tmpdir, 0), "bitcoin.conf"), 'a', encoding='utf8') as f: + f.write("\nrpcwhitelistdefault=0\n") + f.write("rpcauth={}:{}\n".format(self.settings[0], self.settings[1])) + f.write("rpcwhitelist={}:{}\n".format(self.settings[0], self.settings[3])) + f.write("rpcauth={}:{}\n".format(self.settings_forbidden[0], self.settings_forbidden[1])) + f.write("rpcwhitelist={}:{}\n".format(self.settings_forbidden[0], self.settings_forbidden[3])) + + def run_test(self): + self.log.info("Test getrpcwhitelist") + whitelisted = {method: None for method in self.settings[3].split(',')} + + # should return allowed rpcs + result = call_rpc(self.nodes[0], self.settings, 'getrpcwhitelist') + assert_equal(200, result['status']) + assert_equal(result['json']['methods'], whitelisted) + # should fail because user has no rpcwhitelist-rpc entry in bitcoin.conf + result = call_rpc(self.nodes[0], self.settings_forbidden, 'getrpcwhitelist') + assert_equal(result['status'], 403) + + # should return a long list of allowed RPC methods (ie, all of them) + result = self.nodes[0].getrpcwhitelist() + assert len(result['methods']) > 10 + +if __name__ == "__main__": + RPCWhitelistTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index f0569ca328..fdc58f6d4f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -224,6 +224,7 @@ BASE_SCRIPTS = [ 'interface_usdt_validation.py', 'rpc_users.py', 'rpc_whitelist.py', + 'rpc_getrpcwhitelist.py', 'feature_proxy.py', 'wallet_signrawtransactionwithwallet.py --legacy-wallet', 'wallet_signrawtransactionwithwallet.py --descriptors', diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index 86e3504974..1e04d17548 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -13,6 +13,7 @@ import sys EXPECTED_CIRCULAR_DEPENDENCIES = ( "chainparamsbase -> common/args -> chainparamsbase", + "httprpc -> rpc/server -> httprpc", "node/blockstorage -> validation -> node/blockstorage", "node/utxo_snapshot -> validation -> node/utxo_snapshot", "qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel",