From: Jason Dillaman Date: Mon, 14 Oct 2019 14:39:54 +0000 (-0400) Subject: mgr: python modules can now perform authorization tests X-Git-Tag: v15.1.0~1092^2~5 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=282c31c383856b45caadcefb876a71e39fe4b219;p=ceph.git mgr: python modules can now perform authorization tests In cases where the python service or individual python modules are enabled via caps, the module might want to perform finer grained tests to ensure specific commands are allowed. An example of this is the 'rbd_support' module limiting access by pools and namespaces. Signed-off-by: Jason Dillaman --- diff --git a/src/mgr/ActivePyModule.cc b/src/mgr/ActivePyModule.cc index b923862fb7ba..402c7cad3a0c 100644 --- a/src/mgr/ActivePyModule.cc +++ b/src/mgr/ActivePyModule.cc @@ -14,8 +14,10 @@ #include "PyFormatter.h" #include "common/debug.h" +#include "mon/MonCommand.h" #include "ActivePyModule.h" +#include "MgrSession.h" #define dout_context g_ceph_context @@ -169,6 +171,8 @@ void ActivePyModule::config_notify() } int ActivePyModule::handle_command( + const ModuleCommand& module_command, + const MgrSession& session, const cmdmap_t &cmdmap, const bufferlist &inbuf, std::stringstream *ds, @@ -192,10 +196,16 @@ int ActivePyModule::handle_command( string instr; inbuf.copy(0, inbuf.length(), instr); + ceph_assert(m_session == nullptr); + m_command_perms = module_command.perm; + m_session = &session; + auto pResult = PyObject_CallMethod(pClassInstance, const_cast("_handle_command"), const_cast("s#O"), instr.c_str(), instr.length(), py_cmd); + m_command_perms.clear(); + m_session = nullptr; Py_DECREF(py_cmd); int r = 0; @@ -227,3 +237,20 @@ void ActivePyModule::get_health_checks(health_check_map_t *checks) checks->merge(health_checks); } +bool ActivePyModule::is_authorized( + const std::map& arguments) const { + if (m_session == nullptr) { + return false; + } + + // No need to pass command prefix here since that would have already been + // tested before command invokation. Instead, only test for service/module + // arguments as defined by the module itself. + MonCommand mon_command {"", "", "", m_command_perms}; + return m_session->caps.is_capable(nullptr, m_session->entity_name, "py", + py_module->get_name(), "", arguments, + mon_command.requires_perm('r'), + mon_command.requires_perm('w'), + mon_command.requires_perm('x'), + m_session->get_peer_addr()); +} diff --git a/src/mgr/ActivePyModule.h b/src/mgr/ActivePyModule.h index beafa629982b..1cbf6d18ac2f 100644 --- a/src/mgr/ActivePyModule.h +++ b/src/mgr/ActivePyModule.h @@ -32,6 +32,8 @@ class ActivePyModule; class ActivePyModules; +class MgrSession; +class ModuleCommand; class ActivePyModule : public PyModuleRunner { @@ -41,6 +43,9 @@ private: // Optional, URI exposed by plugins that implement serve() std::string uri; + std::string m_command_perms; + const MgrSession* m_session = nullptr; + public: ActivePyModule(const PyModuleRef &py_module_, LogChannelRef clog_) @@ -60,6 +65,8 @@ public: std::string *err); int handle_command( + const ModuleCommand& module_command, + const MgrSession& session, const cmdmap_t &cmdmap, const bufferlist &inbuf, std::stringstream *ds, @@ -86,6 +93,9 @@ public: { return uri; } + + bool is_authorized(const std::map& arguments) const; + }; std::string handle_pyerror(); diff --git a/src/mgr/ActivePyModules.cc b/src/mgr/ActivePyModules.cc index 637821564218..e9cef87c697b 100644 --- a/src/mgr/ActivePyModules.cc +++ b/src/mgr/ActivePyModules.cc @@ -911,22 +911,24 @@ void ActivePyModules::set_health_checks(const std::string& module_name, } int ActivePyModules::handle_command( - std::string const &module_name, + const ModuleCommand& module_command, + const MgrSession& session, const cmdmap_t &cmdmap, const bufferlist &inbuf, std::stringstream *ds, std::stringstream *ss) { lock.lock(); - auto mod_iter = modules.find(module_name); + auto mod_iter = modules.find(module_command.module_name); if (mod_iter == modules.end()) { - *ss << "Module '" << module_name << "' is not available"; + *ss << "Module '" << module_command.module_name << "' is not available"; lock.unlock(); return -ENOENT; } lock.unlock(); - return mod_iter->second->handle_command(cmdmap, inbuf, ds, ss); + return mod_iter->second->handle_command(module_command, session, cmdmap, + inbuf, ds, ss); } void ActivePyModules::get_health_checks(health_check_map_t *checks) diff --git a/src/mgr/ActivePyModules.h b/src/mgr/ActivePyModules.h index f36d6593c63b..0ba50ff598b5 100644 --- a/src/mgr/ActivePyModules.h +++ b/src/mgr/ActivePyModules.h @@ -33,6 +33,8 @@ class health_check_map_t; class DaemonServer; +class MgrSession; +class ModuleCommand; class PyModuleRegistry; class ActivePyModules @@ -137,7 +139,8 @@ public: void set_uri(const std::string& module_name, const std::string &uri); int handle_command( - const std::string &module_name, + const ModuleCommand& module_command, + const MgrSession& session, const cmdmap_t &cmdmap, const bufferlist &inbuf, std::stringstream *ds, diff --git a/src/mgr/BaseMgrModule.cc b/src/mgr/BaseMgrModule.cc index fb5d7efe6aa9..b340f74f1a1f 100644 --- a/src/mgr/BaseMgrModule.cc +++ b/src/mgr/BaseMgrModule.cc @@ -1015,6 +1015,42 @@ ceph_get_osd_perf_counters(BaseMgrModule *self, PyObject *args) return self->py_modules->get_osd_perf_counters(query_id); } +static PyObject* +ceph_is_authorized(BaseMgrModule *self, PyObject *args) +{ + PyObject *args_dict = NULL; + if (!PyArg_ParseTuple(args, "O:ceph_is_authorized", &args_dict)) { + return nullptr; + } + + if (!PyDict_Check(args_dict)) { + derr << __func__ << " arg not a dict" << dendl; + Py_RETURN_FALSE; + } + + std::map arguments; + + PyObject *args_list = PyDict_Items(args_dict); + for (int i = 0; i < PyList_Size(args_list); ++i) { + PyObject *kv = PyList_GET_ITEM(args_list, i); + + char *arg_key = nullptr; + char *arg_value = nullptr; + if (!PyArg_ParseTuple(kv, "ss:pair", &arg_key, &arg_value)) { + derr << __func__ << " dict item " << i << " not a size 2 tuple" << dendl; + continue; + } + + arguments[arg_key] = arg_value; + } + + if (self->this_module->is_authorized(arguments)) { + Py_RETURN_TRUE; + } + + Py_RETURN_FALSE; +} + PyMethodDef BaseMgrModule_methods[] = { {"_ceph_get", (PyCFunction)ceph_state_get, METH_VARARGS, "Get a cluster object"}, @@ -1108,6 +1144,9 @@ PyMethodDef BaseMgrModule_methods[] = { {"_ceph_get_osd_perf_counters", (PyCFunction)ceph_get_osd_perf_counters, METH_VARARGS, "Get osd perf counters"}, + {"_ceph_is_authorized", (PyCFunction)ceph_is_authorized, + METH_VARARGS, "Verify the current session caps are valid"}, + {NULL, NULL, 0, NULL} }; diff --git a/src/mgr/DaemonServer.cc b/src/mgr/DaemonServer.cc index 3fd549dcbb80..ccc5bd3eb8bd 100644 --- a/src/mgr/DaemonServer.cc +++ b/src/mgr/DaemonServer.cc @@ -663,6 +663,7 @@ const MonCommand *DaemonServer::_get_mgrcommand( bool DaemonServer::_allowed_command( MgrSession *s, + const string &service, const string &module, const string &prefix, const cmdmap_t& cmdmap, @@ -682,7 +683,7 @@ bool DaemonServer::_allowed_command( bool capable = s->caps.is_capable( g_ceph_context, s->entity_name, - module, "", prefix, param_str_map, + service, module, prefix, param_str_map, cmd_r, cmd_w, cmd_x, s->get_peer_addr()); @@ -801,6 +802,18 @@ bool DaemonServer::handle_command(const ref_t& m) } } +void DaemonServer::log_access_denied( + std::shared_ptr& cmdctx, + MgrSession* session, std::stringstream& ss) { + dout(1) << " access denied" << dendl; + audit_clog->info() << "from='" << session->inst << "' " + << "entity='" << session->entity_name << "' " + << "cmd=" << cmdctx->cmd << ": access denied"; + ss << "access denied: does your client key have mgr caps? " + "See http://docs.ceph.com/docs/master/mgr/administrator/" + "#client-authentication"; +} + bool DaemonServer::_handle_command( std::shared_ptr& cmdctx) { @@ -875,24 +888,32 @@ bool DaemonServer::_handle_command( const MonCommand *mgr_cmd = _get_mgrcommand(prefix, mgr_commands); _generate_command_map(cmdctx->cmdmap, param_str_map); - bool is_allowed; + bool is_allowed = false; + ModuleCommand py_command; if (!mgr_cmd) { - MonCommand py_command = {"", "", "py", "rw"}; - is_allowed = _allowed_command(session, py_command.module, - prefix, cmdctx->cmdmap, param_str_map, &py_command); + // Resolve the command to the name of the module that will + // handle it (if the command exists) + auto py_commands = py_modules.get_py_commands(); + for (const auto &pyc : py_commands) { + auto pyc_prefix = cmddesc_get_prefix(pyc.cmdstring); + if (pyc_prefix == prefix) { + py_command = pyc; + break; + } + } + + MonCommand pyc = {"", "", "py", py_command.perm}; + is_allowed = _allowed_command(session, "py", py_command.module_name, + prefix, cmdctx->cmdmap, param_str_map, + &pyc); } else { // validate user's permissions for requested command - is_allowed = _allowed_command(session, mgr_cmd->module, + is_allowed = _allowed_command(session, mgr_cmd->module, "", prefix, cmdctx->cmdmap, param_str_map, mgr_cmd); } + if (!is_allowed) { - dout(1) << " access denied" << dendl; - audit_clog->info() << "from='" << session->inst << "' " - << "entity='" << session->entity_name << "' " - << "cmd=" << cmdctx->cmd << ": access denied"; - ss << "access denied: does your client key have mgr caps? " - "See http://docs.ceph.com/docs/master/mgr/administrator/" - "#client-authentication"; + log_access_denied(cmdctx, session, ss); cmdctx->reply(-EACCES, ss); return true; } @@ -2211,20 +2232,8 @@ bool DaemonServer::_handle_command( } } - // Resolve the command to the name of the module that will - // handle it (if the command exists) - std::string handler_name; - auto py_commands = py_modules.get_py_commands(); - for (const auto &pyc : py_commands) { - auto pyc_prefix = cmddesc_get_prefix(pyc.cmdstring); - if (pyc_prefix == prefix) { - handler_name = pyc.module_name; - break; - } - } - // Was the command unfound? - if (handler_name.empty()) { + if (py_command.cmdstring.empty()) { ss << "No handler found for '" << prefix << "'"; dout(4) << "No handler found for '" << prefix << "'" << dendl; cmdctx->reply(-EINVAL, ss); @@ -2232,16 +2241,18 @@ bool DaemonServer::_handle_command( } dout(10) << "passing through " << cmdctx->cmdmap.size() << dendl; - finisher.queue(new LambdaContext([this, cmdctx, handler_name, prefix](int r_) { + finisher.queue(new LambdaContext([this, cmdctx, session, py_command, prefix] + (int r_) mutable { std::stringstream ss; // Validate that the module is enabled - PyModuleRef module = py_modules.get_module(handler_name); + auto& py_handler_name = py_command.module_name; + PyModuleRef module = py_modules.get_module(py_handler_name); ceph_assert(module); if (!module->is_enabled()) { - ss << "Module '" << handler_name << "' is not enabled (required by " + ss << "Module '" << py_handler_name << "' is not enabled (required by " "command '" << prefix << "'): use `ceph mgr module enable " - << handler_name << "` to enable it"; + << py_handler_name << "` to enable it"; dout(4) << ss.str() << dendl; cmdctx->reply(-EOPNOTSUPP, ss); return; @@ -2250,7 +2261,7 @@ bool DaemonServer::_handle_command( // Hack: allow the self-test method to run on unhealthy modules. // Fix this in future by creating a special path for self test rather // than having the hook be a normal module command. - std::string self_test_prefix = handler_name + " " + "self-test"; + std::string self_test_prefix = py_handler_name + " " + "self-test"; // Validate that the module is healthy bool accept_command; @@ -2263,13 +2274,13 @@ bool DaemonServer::_handle_command( accept_command = true; } else { accept_command = false; - ss << "Module '" << handler_name << "' has experienced an error and " + ss << "Module '" << py_handler_name << "' has experienced an error and " "cannot handle commands: " << module->get_error_string(); } } else { // Module not loaded accept_command = false; - ss << "Module '" << handler_name << "' failed to load and " + ss << "Module '" << py_handler_name << "' failed to load and " "cannot handle commands: " << module->get_error_string(); } @@ -2281,7 +2292,12 @@ bool DaemonServer::_handle_command( std::stringstream ds; bufferlist inbl = cmdctx->data; - int r = py_modules.handle_command(handler_name, cmdctx->cmdmap, inbl, &ds, &ss); + int r = py_modules.handle_command(py_command, *session, cmdctx->cmdmap, + inbl, &ds, &ss); + if (r == -EACCES) { + log_access_denied(cmdctx, session, ss); + } + cmdctx->odata.append(ds); cmdctx->reply(r, ss); })); diff --git a/src/mgr/DaemonServer.h b/src/mgr/DaemonServer.h index d299aaa4ea15..6fdf1c0756a7 100644 --- a/src/mgr/DaemonServer.h +++ b/src/mgr/DaemonServer.h @@ -84,8 +84,8 @@ protected: static const MonCommand *_get_mgrcommand(const string &cmd_prefix, const std::vector &commands); bool _allowed_command( - MgrSession *s, const string &module, const string &prefix, - const cmdmap_t& cmdmap, + MgrSession *s, const string &service, const string &module, + const string &prefix, const cmdmap_t& cmdmap, const map& param_str_map, const MonCommand *this_cmd); @@ -169,6 +169,9 @@ public: const std::set &changed) override; void schedule_tick(double delay_sec); + + void log_access_denied(std::shared_ptr& cmdctx, + MgrSession* session, std::stringstream& ss); }; #endif diff --git a/src/mgr/MgrSession.h b/src/mgr/MgrSession.h index 0d6ff95716b4..40b50220bddb 100644 --- a/src/mgr/MgrSession.h +++ b/src/mgr/MgrSession.h @@ -24,7 +24,7 @@ struct MgrSession : public RefCountedObject { std::set declared_types; - const entity_addr_t& get_peer_addr() { + const entity_addr_t& get_peer_addr() const { return inst.addr; } diff --git a/src/mgr/PyModuleRegistry.cc b/src/mgr/PyModuleRegistry.cc index f7105b87a705..2897684c4aed 100644 --- a/src/mgr/PyModuleRegistry.cc +++ b/src/mgr/PyModuleRegistry.cc @@ -286,14 +286,16 @@ std::set PyModuleRegistry::probe_modules(const std::string &path) c } int PyModuleRegistry::handle_command( - std::string const &module_name, + const ModuleCommand& module_command, + const MgrSession& session, const cmdmap_t &cmdmap, const bufferlist &inbuf, std::stringstream *ds, std::stringstream *ss) { if (active_modules) { - return active_modules->handle_command(module_name, cmdmap, inbuf, ds, ss); + return active_modules->handle_command(module_command, session, cmdmap, + inbuf, ds, ss); } else { // We do not expect to be called before active modules is up, but // it's straightfoward to handle this case so let's do it. diff --git a/src/mgr/PyModuleRegistry.h b/src/mgr/PyModuleRegistry.h index 94b591fe275e..6d30d126648e 100644 --- a/src/mgr/PyModuleRegistry.h +++ b/src/mgr/PyModuleRegistry.h @@ -27,6 +27,8 @@ #include "ActivePyModules.h" #include "StandbyPyModules.h" +class MgrSession; + /** * This class is responsible for setting up the python runtime environment * and importing the python modules. @@ -138,7 +140,8 @@ public: * return EAGAIN. */ int handle_command( - std::string const &module_name, + const ModuleCommand& module_command, + const MgrSession& session, const cmdmap_t &cmdmap, const bufferlist &inbuf, std::stringstream *ds, diff --git a/src/pybind/mgr/mgr_module.py b/src/pybind/mgr/mgr_module.py index 7480535ac204..b998aa028c38 100644 --- a/src/pybind/mgr/mgr_module.py +++ b/src/pybind/mgr/mgr_module.py @@ -1381,6 +1381,17 @@ class MgrModule(ceph_module.BaseMgrModule): """ return self._ceph_get_osd_perf_counters(query_id) + def is_authorized(self, arguments): + """ + Verifies that the current session caps permit executing the py service + or current module with the provided arguments. This provides a generic + way to allow modules to restrict by more fine-grained controls (e.g. + pools). + + :param arguments: dict of key/value arguments to test + """ + return self._ceph_is_authorized(arguments) + class PersistentStoreDict(object): def __init__(self, mgr, prefix):