]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.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>
Thu, 9 Jan 2020 18:59:36 +0000 (13:59 -0500)
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>
(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

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 c3780269ee047bc6d1f0f0ff4f15a164dde82e4e..410b8d817996c0b97b86068e388c4eafb6f87807 100644 (file)
@@ -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<std::string, std::string>& arguments) const;
+
 };
 
 std::string handle_pyerror();
index ec7d941c8f5486ce3c577a17902a157f9c080157..71c767915b2ffa898eca34251c298e97458d3c41 100644 (file)
@@ -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)
index b3feb0be4e64cc155a9b9e713eb68adcd6c02a8e..ab9b637d9ee9d86d97b63c09cd104606039e1d97 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 7415db0ba4c47e66c9f437b336cd528baf5a9bdb..84886ae2c7198745a5795e2b7c119a9977b12e61 100644 (file)
@@ -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<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"},
@@ -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}
 };
 
index 2c6c2aee6128f49a3ce6e23eccf474663ed09543..3ffad5d418727fae1fb274c84c68a0d9a8048f5a 100644 (file)
@@ -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<CommandContext>& 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<CommandContext>& 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);
   }));
index 310a030b1b59adcd642edaafb28a34d356404eb3..80ceaa6b1563f419df2d42b336a5c6650d1e8b94 100644 (file)
@@ -83,8 +83,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 c2ae345cb7f1050e677f4e77d8c24cfa898bc7a4..f5ad933821a4609cbc078395139ea308edd105c9 100644 (file)
@@ -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;
   }
 };
index ad275e38d4629aebddb27c6b8452373bcae7697a..1ff92cd9ee4d3b53489adeb455d8f3c474212a0f 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 52e9907e361b22d167dc0e67388d00eaa2649698..89080cdbc8e7de4e773b970247398c1e84312eb8 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 9d4bcb41cc77ff9cdeae8c3c811f6062d880f921..3aeb858ce661143d34a1b313fd48de6e1af686e2 100644 (file)
@@ -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):