diff --git a/NEWS b/NEWS index 2a09b2fbd19795848a6026ed9c024d60b83e124d..65ac4d5319021a6afde7a0c8711132b1c9fff5d9 100644 --- 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 ------------------- diff --git a/lib/rapi/testutils.py b/lib/rapi/testutils.py index ccb8b632f9d886f3b1802a52c67f4f11510f8ce4..cdc34dc3172a92838d6ea11ae9ea714ce5c412c5 100644 --- a/lib/rapi/testutils.py +++ b/lib/rapi/testutils.py @@ -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. diff --git a/lib/server/rapi.py b/lib/server/rapi.py index 65162ee936447e079d93eeb08ebdedfdf18c0ee5..47ebb5b0856b9e3bb57080547d737b25a1083a47 100644 --- a/lib/server/rapi.py +++ b/lib/server/rapi.py @@ -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, diff --git a/man/ganeti-rapi.rst b/man/ganeti-rapi.rst index ad78e856a29855bca261b8993c373a99783e2d68..7b5ce1fc273bd7892ac9f166d41458cad11bf481 100644 --- a/man/ganeti-rapi.rst +++ b/man/ganeti-rapi.rst @@ -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: diff --git a/test/py/ganeti.server.rapi_unittest.py b/test/py/ganeti.server.rapi_unittest.py index 735c3006cfdaf58ea8dfeac1c617ac53a07816a0..bf73da9a184f5a641a1b1d448fce1ec8aa96af94 100755 --- a/test/py/ganeti.server.rapi_unittest.py +++ b/test/py/ganeti.server.rapi_unittest.py @@ -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: