RAPI: Add flag to require authentication
authorMichael Hanselmann <hansmi@google.com>
Wed, 20 Feb 2013 17:01:56 +0000 (18:01 +0100)
committerMichael Hanselmann <hansmi@google.com>
Fri, 22 Feb 2013 10:51:07 +0000 (11:51 +0100)
Most RAPI resources do not require authentication for the “GET” method.
In some setups it can be desirable to always require authentication.
This patch adds a command line parameter to always require it.

Some unrelated minor typos in the “ganeti-rapi” man page are also fixed.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>

NEWS
lib/rapi/testutils.py
lib/server/rapi.py
man/ganeti-rapi.rst
test/py/ganeti.server.rapi_unittest.py

diff --git a/NEWS b/NEWS
index 2a09b2f..65ac4d5 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,16 @@ News
 ====
 
 
+Version 2.8.0 beta1
+-------------------
+
+*(unreleased)*
+
+- The :doc:`Remote API <rapi>` daemon now supports a command line flag
+  to always require authentication, ``--require-authentication``. It can
+  be specified in ``$sysconfdir/default/ganeti``.
+
+
 Version 2.7.0 beta1
 -------------------
 
index ccb8b63..cdc34dc 100644 (file)
@@ -210,7 +210,7 @@ class _RapiMock:
   """Mocking out the RAPI server parts.
 
   """
-  def __init__(self, user_fn, luxi_client):
+  def __init__(self, user_fn, luxi_client, reqauth=False):
     """Initialize this class.
 
     @type user_fn: callable
@@ -219,7 +219,7 @@ class _RapiMock:
 
     """
     self.handler = \
-      server.rapi.RemoteApiHandler(user_fn, _client_cls=luxi_client)
+      server.rapi.RemoteApiHandler(user_fn, reqauth, _client_cls=luxi_client)
 
   def FetchResponse(self, path, method, headers, request_body):
     """This is a callback method used to fetch a response.
index 65162ee..47ebb5b 100644 (file)
@@ -73,12 +73,14 @@ class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
   """
   AUTH_REALM = "Ganeti Remote API"
 
-  def __init__(self, user_fn, _client_cls=None):
+  def __init__(self, user_fn, reqauth, _client_cls=None):
     """Initializes this class.
 
     @type user_fn: callable
     @param user_fn: Function receiving username as string and returning
       L{http.auth.PasswordFileUser} or C{None} if user is not found
+    @type reqauth: bool
+    @param reqauth: Whether to require authentication
 
     """
     # pylint: disable=W0233
@@ -88,6 +90,7 @@ class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
     self._client_cls = _client_cls
     self._resmap = connector.Mapper()
     self._user_fn = user_fn
+    self._reqauth = reqauth
 
   @staticmethod
   def FormatErrorMessage(values):
@@ -143,7 +146,7 @@ class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
     """Determine whether authentication is required.
 
     """
-    return bool(self._GetRequestContext(req).handler_access)
+    return self._reqauth or bool(self._GetRequestContext(req).handler_access)
 
   def Authenticate(self, req, username, password):
     """Checks whether a user can access a resource.
@@ -324,7 +327,7 @@ def PrepRapi(options, _):
 
   users = RapiUsers()
 
-  handler = RemoteApiHandler(users.Get)
+  handler = RemoteApiHandler(users.Get, options.reqauth)
 
   # Setup file watcher (it'll be driven by asyncore)
   SetupFileWatcher(pathutils.RAPI_USERS_FILE,
@@ -360,6 +363,10 @@ def Main():
                                  usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
                                  version="%%prog (ganeti) %s" %
                                  constants.RELEASE_VERSION)
+  parser.add_option("--require-authentication", dest="reqauth",
+                    default=False, action="store_true",
+                    help=("Disable anonymous HTTP requests and require"
+                          " authentication"))
 
   daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
                      default_ssl_cert=pathutils.RAPI_CERT_FILE,
index ad78e85..7b5ce1f 100644 (file)
@@ -9,8 +9,8 @@ ganeti-rapi - Ganeti remote API daemon
 Synopsis
 --------
 
-**ganeti-rapi** [-d] [-f] [\--no-ssl] [-K *SSL_KEY_FILE*] [-C
-*SSL_CERT_FILE*]
+| **ganeti-rapi** [-d] [-f] [\--no-ssl] [-K *SSL_KEY_FILE*]
+| [-C *SSL_CERT_FILE*] [\--require-authentication]
 
 DESCRIPTION
 -----------
@@ -23,7 +23,7 @@ uses SSL encryption. This can be disabled by passing the
 ``--no-ssl`` option, or alternatively the certificate used can be
 changed via the ``-C`` option and the key via the ``-K`` option.
 
-The daemon will listen to the "ganeti-rapi" tcp port, as listed in the
+The daemon will listen to the "ganeti-rapi" TCP port, as listed in the
 system services database, or if not defined, to port 5080 by default.
 
 See the *Ganeti remote API* documentation for further information.
@@ -36,11 +36,12 @@ ACCESS CONTROLS
 
 Most query operations are allowed without authentication. Only the
 modification operations require authentication, in the form of basic
-authentication.
+authentication. Specify the ``--require-authentication`` command line
+flag to always require authentication.
 
 The users and their rights are defined in the
-``@LOCALSTATEDIR@/lib/ganeti/rapi/users`` file. Format of this file is
-described in the Ganeti documentation (``rapi.html``).
+``@LOCALSTATEDIR@/lib/ganeti/rapi/users`` file. The format of this file
+is described in the Ganeti documentation (``rapi.html``).
 
 .. vim: set textwidth=72 :
 .. Local Variables:
index 735c300..bf73da9 100755 (executable)
@@ -51,8 +51,9 @@ class TestRemoteApiHandler(unittest.TestCase):
     return None
 
   def _Test(self, method, path, headers, reqbody,
-            user_fn=NotImplemented, luxi_client=NotImplemented):
-    rm = rapi.testutils._RapiMock(user_fn, luxi_client)
+            user_fn=NotImplemented, luxi_client=NotImplemented,
+            reqauth=False):
+    rm = rapi.testutils._RapiMock(user_fn, luxi_client, reqauth=reqauth)
 
     (resp_code, resp_headers, resp_body) = \
       rm.FetchResponse(path, method, http.ParseHeaders(StringIO(headers)),
@@ -70,6 +71,10 @@ class TestRemoteApiHandler(unittest.TestCase):
     self.assertEqual(code, http.HTTP_OK)
     self.assertTrue(data is None)
 
+  def testRootReqAuth(self):
+    (code, _, _) = self._Test(http.HTTP_GET, "/", "", None, reqauth=True)
+    self.assertEqual(code, http.HttpUnauthorized.code)
+
   def testVersion(self):
     (code, _, data) = self._Test(http.HTTP_GET, "/version", "", None)
     self.assertEqual(code, http.HTTP_OK)
@@ -235,13 +240,14 @@ class TestRemoteApiHandler(unittest.TestCase):
     path = "/2/instances/inst1.example.com/console"
 
     for method in rapi.baserlib._SUPPORTED_METHODS:
-      # No authorization
-      (code, _, _) = self._Test(method, path, "", "")
+      for reqauth in [False, True]:
+        # No authorization
+        (code, _, _) = self._Test(method, path, "", "", reqauth=reqauth)
 
-      if method == http.HTTP_GET:
-        self.assertEqual(code, http.HttpUnauthorized.code)
-      else:
-        self.assertEqual(code, http.HttpNotImplemented.code)
+        if method == http.HTTP_GET or reqauth:
+          self.assertEqual(code, http.HttpUnauthorized.code)
+        else:
+          self.assertEqual(code, http.HttpNotImplemented.code)
 
 
 class _FakeLuxiClientForQuery: