]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr: python modules can now perform authorization tests
authorJason Dillaman <dillaman@redhat.com>
Mon, 14 Oct 2019 14:39:54 +0000 (10:39 -0400)
committerJason Dillaman <dillaman@redhat.com>
Tue, 29 Oct 2019 12:35:03 +0000 (08:35 -0400)
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 <dillaman@redhat.com>
src/mgr/ActivePyModule.cc
src/mgr/ActivePyModule.h
src/mgr/ActivePyModules.cc
src/mgr/ActivePyModules.h
src/mgr/BaseMgrModule.cc
src/mgr/DaemonServer.cc
src/mgr/DaemonServer.h
src/mgr/MgrSession.h
src/mgr/PyModuleRegistry.cc
src/mgr/PyModuleRegistry.h
src/pybind/mgr/mgr_module.py

index b923862fb7baec72eccf559533dfdf1bbfe5c56c..402c7cad3a0ccbe91562cf6c8c5b8009b28a5a2c 100644 (file)
 #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<char*>("_handle_command"), const_cast<char*>("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<std::string, std::string>& 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());
+}
index beafa629982bc07aa23b2a6b19d6ee45adea2097..1cbf6d18ac2f4e1d9feec69102e7031ba61fb820 100644 (file)
@@ -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<std::string, std::string>& arguments) const;
+
 };
 
 std::string handle_pyerror();
index 637821564218b5e34af1abddaf4c4774975a52dd..e9cef87c697b4dbd519edf873cb669e6c92137d8 100644 (file)
@@ -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)
index f36d6593c63bc430255089342f9eb7bbe96d3465..0ba50ff598b53012520cbad04d6ea5535699bd26 100644 (file)
@@ -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,
index fb5d7efe6aa9ece96c1de160182f7e98b81d6d45..b340f74f1a1f4666317d6282449759c583b4c8fd 100644 (file)
@@ -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<std::string, std::string> 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}
 };
 
index 3fd549dcbb80a42f2f9194abb317d3cb0c622003..ccc5bd3eb8bd2cd1edbd5e1b11f5eb24a4db7143 100644 (file)
@@ -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<MMgrCommand>& m)
   }
 }
 
+void DaemonServer::log_access_denied(
+    std::shared_ptr<CommandContext>& 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<CommandContext>& 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);
   }));
index d299aaa4ea1511335b7038884f1dfc4f4539abf9..6fdf1c0756a76ac910cda33d574a44584beae99a 100644 (file)
@@ -84,8 +84,8 @@ protected:
   static const MonCommand *_get_mgrcommand(const string &cmd_prefix,
                                            const std::vector<MonCommand> &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<string,string>& param_str_map,
     const MonCommand *this_cmd);
 
@@ -169,6 +169,9 @@ public:
                           const std::set <std::string> &changed) override;
 
   void schedule_tick(double delay_sec);
+
+  void log_access_denied(std::shared_ptr<CommandContext>& cmdctx,
+                         MgrSession* session, std::stringstream& ss);
 };
 
 #endif
index 0d6ff95716b4714b9373700c035c524d9fabc377..40b50220bddb17c911c9ed2328bf8dae45e78ba6 100644 (file)
@@ -24,7 +24,7 @@ struct MgrSession : public RefCountedObject {
 
   std::set<std::string> declared_types;
 
-  const entity_addr_t& get_peer_addr() {
+  const entity_addr_t& get_peer_addr() const {
     return inst.addr;
   }
 
index f7105b87a70584a4292e1a23eb95378b496f03e6..2897684c4aedd20e6496477678dfe922617562e6 100644 (file)
@@ -286,14 +286,16 @@ std::set<std::string> 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.
index 94b591fe275ecd9d47e2c81967ea9356d72dce50..6d30d126648eb658d9df70eef1e25c95fb8b5805 100644 (file)
@@ -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,
index 7480535ac2048dca578fd5c6279751fc86ca3880..b998aa028c38ff06f0f233ed9e03fc2e8989fa87 100644 (file)
@@ -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):