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: v14.2.8~20^2~55^2~6 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=3940af0598c89b3ecaa703d7d1e2290324e5792b;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 (cherry picked from commit 282c31c383856b45caadcefb876a71e39fe4b219) Conflicts: src/mgr/ActivePyModules.cc: tweaked due to changes introduced in 4878509652 src/mgr/DaemonServer.cc: tweaked due to changes introduced in 4878509652 src/mgr/MgrSession.h: trivial resolition --- diff --git a/src/mgr/ActivePyModule.cc b/src/mgr/ActivePyModule.cc index b923862fb7b..402c7cad3a0 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 c3780269ee0..410b8d81799 100644 --- a/src/mgr/ActivePyModule.h +++ b/src/mgr/ActivePyModule.h @@ -33,6 +33,8 @@ class ActivePyModule; class ActivePyModules; +class MgrSession; +class ModuleCommand; class ActivePyModule : public PyModuleRunner { @@ -42,6 +44,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_) @@ -61,6 +66,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, @@ -87,6 +94,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 ec7d941c8f5..71c767915b2 100644 --- a/src/mgr/ActivePyModules.cc +++ b/src/mgr/ActivePyModules.cc @@ -925,22 +925,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); + lock.lock(); + auto mod_iter = modules.find(module_command.module_name); if (mod_iter == modules.end()) { - *ss << "Module '" << module_name << "' is not available"; - lock.Unlock(); + *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); + lock.unlock(); + 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 b3feb0be4e6..ab9b637d9ee 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 7415db0ba4c..84886ae2c71 100644 --- a/src/mgr/BaseMgrModule.cc +++ b/src/mgr/BaseMgrModule.cc @@ -1001,6 +1001,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"}, @@ -1091,6 +1127,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 2c6c2aee612..3ffad5d4187 100644 --- a/src/mgr/DaemonServer.cc +++ b/src/mgr/DaemonServer.cc @@ -681,6 +681,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, @@ -700,7 +701,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()); @@ -785,6 +786,18 @@ bool DaemonServer::handle_command(MCommand *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->cmdmap << ": 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( MCommand *m, std::shared_ptr& cmdctx) @@ -854,24 +867,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=" << m->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; } @@ -2180,37 +2201,27 @@ 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); return true; } - dout(4) << "passing through " << cmdctx->cmdmap.size() << dendl; - finisher.queue(new FunctionContext([this, cmdctx, handler_name, prefix](int r_) { + dout(10) << "passing through " << cmdctx->cmdmap.size() << dendl; + finisher.queue(new FunctionContext([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; @@ -2219,7 +2230,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; @@ -2232,13 +2243,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(); } @@ -2250,7 +2261,12 @@ bool DaemonServer::_handle_command( std::stringstream ds; bufferlist inbl = cmdctx->m->get_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 310a030b1b5..80ceaa6b156 100644 --- a/src/mgr/DaemonServer.h +++ b/src/mgr/DaemonServer.h @@ -83,8 +83,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 c2ae345cb7f..f5ad933821a 100644 --- a/src/mgr/MgrSession.h +++ b/src/mgr/MgrSession.h @@ -27,7 +27,7 @@ struct MgrSession : public RefCountedObject { explicit MgrSession(CephContext *cct) : RefCountedObject(cct, 0) {} ~MgrSession() override {} - 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 ad275e38d46..1ff92cd9ee4 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 52e9907e361..89080cdbc8e 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 9d4bcb41cc7..3aeb858ce66 100644 --- a/src/pybind/mgr/mgr_module.py +++ b/src/pybind/mgr/mgr_module.py @@ -1318,6 +1318,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):