Commit 5e12acfe authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

rapi: Add new user option for querying



This was requested in issue 301. Before this patch, requests to
“/2/query/*” and “/2/instances/*/console” would require authentication
with a user with write access. Since that is not strictly necessary, a
new user option named “read” is added.

Console information can also be retrieved as a normal query, therefore
the change applies there too.

This was the first user option to be added after “write”, therefore
quite a few changes were necessary. Documentation, including NEWS, is
updated as well.
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarGuido Trotter <ultrotter@google.com>
parent 7b0476cf
...@@ -29,6 +29,10 @@ Version 2.7.0 beta1 ...@@ -29,6 +29,10 @@ Version 2.7.0 beta1
``gnt-node add`` now invokes a new tool on the destination node, named ``gnt-node add`` now invokes a new tool on the destination node, named
``prepare-node-join``, to configure the SSH daemon. Paramiko is no ``prepare-node-join``, to configure the SSH daemon. Paramiko is no
longer necessary to configure nodes' SSH daemons via ``gnt-node add``. longer necessary to configure nodes' SSH daemons via ``gnt-node add``.
- A new user option, :pyeval:`rapi.RAPI_ACCESS_READ`, has been added
for RAPI users. It allows granting permissions to query for
information to a specific user without giving
:pyeval:`rapi.RAPI_ACCESS_WRITE` permissions.
Version 2.6.1 Version 2.6.1
......
...@@ -24,12 +24,24 @@ Users and passwords ...@@ -24,12 +24,24 @@ Users and passwords
``/var/lib/ganeti/rapi/users``) on startup. Changes to the file will be ``/var/lib/ganeti/rapi/users``) on startup. Changes to the file will be
read automatically. read automatically.
Each line consists of two or three fields separated by whitespace. The Lines starting with the hash sign (``#``) are treated as comments. Each
first two fields are for username and password. The third field is line consists of two or three fields separated by whitespace. The first
optional and can be used to specify per-user options. Currently, two fields are for username and password. The third field is optional
``write`` is the only option supported and enables the user to execute and can be used to specify per-user options (separated by comma without
operations modifying the cluster. Lines starting with the hash sign spaces). Available options:
(``#``) are treated as comments.
.. pyassert::
rapi.RAPI_ACCESS_ALL == set([
rapi.RAPI_ACCESS_WRITE,
rapi.RAPI_ACCESS_READ,
])
:pyeval:`rapi.RAPI_ACCESS_WRITE`
Enables the user to execute operations modifying the cluster. Implies
:pyeval:`rapi.RAPI_ACCESS_READ` access.
:pyeval:`rapi.RAPI_ACCESS_READ`
Allow access to operations querying for information.
Passwords can either be written in clear text or as a hash. Clear text Passwords can either be written in clear text or as a hash. Clear text
passwords may not start with an opening brace (``{``) or they must be passwords may not start with an opening brace (``{``) or they must be
...@@ -51,6 +63,12 @@ Example:: ...@@ -51,6 +63,12 @@ Example::
# Hashed password for Jessica # Hashed password for Jessica
jessica {HA1}7046452df2cbb530877058712cf17bd4 write jessica {HA1}7046452df2cbb530877058712cf17bd4 write
# Monitoring can query for values
monitoring {HA1}ec018ffe72b8e75bb4d508ed5b6d079c query
# A user who can query and write
superuser {HA1}ec018ffe72b8e75bb4d508ed5b6d079c query,write
.. [#pwhash] Using the MD5 hash of username, realm and password is .. [#pwhash] Using the MD5 hash of username, realm and password is
described in :rfc:`2617` ("HTTP Authentication"), sections 3.2.2.2 described in :rfc:`2617` ("HTTP Authentication"), sections 3.2.2.2
...@@ -1121,7 +1139,15 @@ Job result: ...@@ -1121,7 +1139,15 @@ Job result:
Request information for connecting to instance's console. Request information for connecting to instance's console.
Supports the following commands: ``GET``. .. pyassert::
not (hasattr(rlib2.R_2_instances_name_console, "PUT") or
hasattr(rlib2.R_2_instances_name_console, "POST") or
hasattr(rlib2.R_2_instances_name_console, "DELETE"))
Supports the following commands: ``GET``. Requires authentication with
one of the following options:
:pyeval:`utils.CommaJoin(rlib2.R_2_instances_name_console.GET_ACCESS)`.
``GET`` ``GET``
~~~~~~~ ~~~~~~~
...@@ -1640,7 +1666,15 @@ pages and using ``/2/query/[resource]/fields``. The resource is one of ...@@ -1640,7 +1666,15 @@ pages and using ``/2/query/[resource]/fields``. The resource is one of
:pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the :doc:`query2 :pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the :doc:`query2
design document <design-query2>` for more details. design document <design-query2>` for more details.
Supports the following commands: ``GET``, ``PUT``. .. pyassert::
(rlib2.R_2_query.GET_ACCESS == rlib2.R_2_query.PUT_ACCESS and
not (hasattr(rlib2.R_2_query, "POST") or
hasattr(rlib2.R_2_query, "DELETE")))
Supports the following commands: ``GET``, ``PUT``. Requires
authentication with one of the following options:
:pyeval:`utils.CommaJoin(rlib2.R_2_query.GET_ACCESS)`.
``GET`` ``GET``
~~~~~~~ ~~~~~~~
......
...@@ -21,3 +21,9 @@ ...@@ -21,3 +21,9 @@
"""Ganeti RAPI module""" """Ganeti RAPI module"""
RAPI_ACCESS_WRITE = "write" RAPI_ACCESS_WRITE = "write"
RAPI_ACCESS_READ = "read"
RAPI_ACCESS_ALL = frozenset([
RAPI_ACCESS_WRITE,
RAPI_ACCESS_READ,
])
...@@ -1226,7 +1226,7 @@ class R_2_instances_name_console(baserlib.ResourceBase): ...@@ -1226,7 +1226,7 @@ class R_2_instances_name_console(baserlib.ResourceBase):
"""/2/instances/[instance_name]/console resource. """/2/instances/[instance_name]/console resource.
""" """
GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
GET_OPCODE = opcodes.OpInstanceConsole GET_OPCODE = opcodes.OpInstanceConsole
def GET(self): def GET(self):
...@@ -1278,7 +1278,8 @@ class R_2_query(baserlib.ResourceBase): ...@@ -1278,7 +1278,8 @@ class R_2_query(baserlib.ResourceBase):
""" """
# Results might contain sensitive information # Results might contain sensitive information
GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
PUT_ACCESS = GET_ACCESS
GET_OPCODE = opcodes.OpQuery GET_OPCODE = opcodes.OpQuery
PUT_OPCODE = opcodes.OpQuery PUT_OPCODE = opcodes.OpQuery
......
...@@ -168,67 +168,80 @@ class TestRemoteApiHandler(unittest.TestCase): ...@@ -168,67 +168,80 @@ class TestRemoteApiHandler(unittest.TestCase):
else: else:
return None return None
def _LookupUserWithWrite(name): for access in [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]:
if name == username: def _LookupUserWithWrite(name):
return http.auth.PasswordFileUser(name, password, [ if name == username:
rapi.RAPI_ACCESS_WRITE, return http.auth.PasswordFileUser(name, password, [
]) access,
else: ])
return None
for qr in constants.QR_VIA_RAPI:
# The /2/query resource has somewhat special rules for authentication as
# it can be used to retrieve critical information
path = "/2/query/%s" % qr
for method in rapi.baserlib._SUPPORTED_METHODS:
# No authorization
(code, _, _) = self._Test(method, path, "", "")
if method in (http.HTTP_DELETE, http.HTTP_POST):
self.assertEqual(code, http.HttpNotImplemented.code)
continue
self.assertEqual(code, http.HttpUnauthorized.code)
# Incorrect user
(code, _, _) = self._Test(method, path, header_fn(True), "",
user_fn=self._LookupWrongUser)
self.assertEqual(code, http.HttpUnauthorized.code)
# User has no write access, but the password is correct
(code, _, _) = self._Test(method, path, header_fn(True), "",
user_fn=_LookupUserNoWrite)
self.assertEqual(code, http.HttpForbidden.code)
# Wrong password and no write access
(code, _, _) = self._Test(method, path, header_fn(False), "",
user_fn=_LookupUserNoWrite)
self.assertEqual(code, http.HttpUnauthorized.code)
# Wrong password with write access
(code, _, _) = self._Test(method, path, header_fn(False), "",
user_fn=_LookupUserWithWrite)
self.assertEqual(code, http.HttpUnauthorized.code)
# Prepare request information
if method == http.HTTP_PUT:
reqpath = path
body = serializer.DumpJson({
"fields": ["name"],
})
elif method == http.HTTP_GET:
reqpath = "%s?fields=name" % path
body = ""
else: else:
self.fail("Unknown method '%s'" % method) return None
# User has write access, password is correct for qr in constants.QR_VIA_RAPI:
(code, _, data) = self._Test(method, reqpath, header_fn(True), body, # The /2/query resource has somewhat special rules for authentication as
user_fn=_LookupUserWithWrite, # it can be used to retrieve critical information
luxi_client=_FakeLuxiClientForQuery) path = "/2/query/%s" % qr
self.assertEqual(code, http.HTTP_OK)
self.assertTrue(objects.QueryResponse.FromDict(data)) for method in rapi.baserlib._SUPPORTED_METHODS:
# No authorization
(code, _, _) = self._Test(method, path, "", "")
if method in (http.HTTP_DELETE, http.HTTP_POST):
self.assertEqual(code, http.HttpNotImplemented.code)
continue
self.assertEqual(code, http.HttpUnauthorized.code)
# Incorrect user
(code, _, _) = self._Test(method, path, header_fn(True), "",
user_fn=self._LookupWrongUser)
self.assertEqual(code, http.HttpUnauthorized.code)
# User has no write access, but the password is correct
(code, _, _) = self._Test(method, path, header_fn(True), "",
user_fn=_LookupUserNoWrite)
self.assertEqual(code, http.HttpForbidden.code)
# Wrong password and no write access
(code, _, _) = self._Test(method, path, header_fn(False), "",
user_fn=_LookupUserNoWrite)
self.assertEqual(code, http.HttpUnauthorized.code)
# Wrong password with write access
(code, _, _) = self._Test(method, path, header_fn(False), "",
user_fn=_LookupUserWithWrite)
self.assertEqual(code, http.HttpUnauthorized.code)
# Prepare request information
if method == http.HTTP_PUT:
reqpath = path
body = serializer.DumpJson({
"fields": ["name"],
})
elif method == http.HTTP_GET:
reqpath = "%s?fields=name" % path
body = ""
else:
self.fail("Unknown method '%s'" % method)
# User has write access, password is correct
(code, _, data) = self._Test(method, reqpath, header_fn(True), body,
user_fn=_LookupUserWithWrite,
luxi_client=_FakeLuxiClientForQuery)
self.assertEqual(code, http.HTTP_OK)
self.assertTrue(objects.QueryResponse.FromDict(data))
def testConsole(self):
path = "/2/instances/inst1.example.com/console"
for method in rapi.baserlib._SUPPORTED_METHODS:
# No authorization
(code, _, _) = self._Test(method, path, "", "")
if method == http.HTTP_GET:
self.assertEqual(code, http.HttpUnauthorized.code)
else:
self.assertEqual(code, http.HttpNotImplemented.code)
class _FakeLuxiClientForQuery: class _FakeLuxiClientForQuery:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment