[PATCH] Block access for non-root users

From: Aline Manera <alinefm@br.ibm.com> Non-root users must have restricted access to Kimchi. This patch block non-root urser to: - get or create debug reports; - reboot or shutdown host system; - create, activate/deactivate or delete networks; - create, activate/deactivate or delete storage pools; - update or delete templates; - create, start/stop/connect or delete vms. It also updates the tests cases to always run as a root user. And add authorization tests to make sure non-root users have restricted access to kimchi. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/control/debugreports.py | 2 +- src/kimchi/control/host.py | 2 +- src/kimchi/control/networks.py | 2 +- src/kimchi/control/storagepools.py | 2 +- src/kimchi/control/templates.py | 2 +- src/kimchi/control/vms.py | 2 +- tests/Makefile.am | 1 + tests/test_authorization.py | 124 ++++++++++++++++++++++++++++++++++++ tests/utils.py | 23 ++++++- 9 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 tests/test_authorization.py diff --git a/src/kimchi/control/debugreports.py b/src/kimchi/control/debugreports.py index 324d826..57dc0f3 100644 --- a/src/kimchi/control/debugreports.py +++ b/src/kimchi/control/debugreports.py @@ -26,7 +26,7 @@ from kimchi.control.utils import internal_redirect from kimchi.control.utils import UrlSubNode -@UrlSubNode("debugreports", True) +@UrlSubNode("debugreports", True, ['GET', 'POST']) class DebugReports(AsyncCollection): def __init__(self, model): super(DebugReports, self).__init__(model) diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 0852bd0..41e0040 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -31,7 +31,7 @@ from kimchi.exception import OperationFailed from kimchi.template import render -@UrlSubNode("host", True) +@UrlSubNode("host", True, ['POST']) class Host(Resource): def __init__(self, model, id=None): super(Host, self).__init__(model, id) diff --git a/src/kimchi/control/networks.py b/src/kimchi/control/networks.py index 8510e49..3a02f60 100644 --- a/src/kimchi/control/networks.py +++ b/src/kimchi/control/networks.py @@ -25,7 +25,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.utils import UrlSubNode -@UrlSubNode("networks", True) +@UrlSubNode("networks", True, ['POST', 'DELETE']) class Networks(Collection): def __init__(self, model): super(Networks, self).__init__(model) diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index ea19609..7e6bdd7 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -34,7 +34,7 @@ from kimchi.model.storagepools import ISO_POOL_NAME from kimchi.control.utils import UrlSubNode -@UrlSubNode("storagepools", True) +@UrlSubNode("storagepools", True, ['POST', 'DELETE']) class StoragePools(Collection): def __init__(self, model): super(StoragePools, self).__init__(model) diff --git a/src/kimchi/control/templates.py b/src/kimchi/control/templates.py index 58dafcc..8135e32 100644 --- a/src/kimchi/control/templates.py +++ b/src/kimchi/control/templates.py @@ -25,7 +25,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.utils import UrlSubNode -@UrlSubNode("templates", True) +@UrlSubNode("templates", True, ['PUT', 'DELETE']) class Templates(Collection): def __init__(self, model): super(Templates, self).__init__(model) diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py index 60fc8ff..a74ce27 100644 --- a/src/kimchi/control/vms.py +++ b/src/kimchi/control/vms.py @@ -27,7 +27,7 @@ from kimchi.control.utils import internal_redirect, UrlSubNode from kimchi.control.vm import sub_nodes -@UrlSubNode("vms", True) +@UrlSubNode("vms", True, ['POST', 'PUT', 'DELETE']) class VMs(Collection): def __init__(self, model): super(VMs, self).__init__(model) diff --git a/tests/Makefile.am b/tests/Makefile.am index 0487ffc..e8db05c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -24,6 +24,7 @@ EXTRA_DIST = \ Makefile.am \ run_tests.sh.in \ iso_gen.py \ + test_authorization.py \ test_config.py.in \ test_exception.py \ test_mockmodel.py \ diff --git a/tests/test_authorization.py b/tests/test_authorization.py new file mode 100644 index 0000000..7f939a8 --- /dev/null +++ b/tests/test_authorization.py @@ -0,0 +1,124 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import json +import os +import unittest + + +from functools import partial + + +import kimchi.mockmodel +from utils import get_free_port, patch_auth, request +from utils import run_server + + +test_server = None +model = None +host = None +port = None +ssl_port = None + + +def setUpModule(): + global test_server, model, host, port, ssl_port + + patch_auth(sudo = False) + model = kimchi.mockmodel.MockModel('/tmp/obj-store-test') + host = '127.0.0.1' + port = get_free_port('http') + test_server = run_server(host, port, None, test_mode=True, model=model) + + +def tearDownModule(): + test_server.stop() + os.unlink('/tmp/obj-store-test') + + +class AuthorizationTests(unittest.TestCase): + def setUp(self): + self.request = partial(request, host, port) + model.reset() + + def test_nonroot_access(self): + # Non-root users can access static host information + resp = self.request('/host', '{}', 'GET') + self.assertEquals(200, resp.status) + + # Non-root users can access host stats + resp = self.request('/host/stats', '{}', 'GET') + self.assertEquals(200, resp.status) + + # Non-root users can not reboot/shutdown host system + resp = self.request('/host/reboot', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/host/shutdown', '{}', 'POST') + self.assertEquals(401, resp.status) + + # Non-root users can not get or debug reports + resp = self.request('/debugreports', '{}', 'GET') + self.assertEquals(401, resp.status) + resp = self.request('/debugreports', '{}', 'POST') + self.assertEquals(401, resp.status) + + # Non-root users can not create or delete network (only get) + resp = self.request('/networks', '{}', 'GET') + self.assertEquals(200, resp.status) + resp = self.request('/networks', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/networks/default/activate', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/networks/default', '{}', 'DELETE') + self.assertEquals(401, resp.status) + + # Non-root users can not create or delete storage pool (only get) + resp = self.request('/storagepools', '{}', 'GET') + self.assertEquals(200, resp.status) + resp = self.request('/storagepools', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/storagepools/default/activate', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/storagepools/default', '{}', 'DELETE') + self.assertEquals(401, resp.status) + + # Non-root users can not update or delete a template + # but he can get and create a new one + resp = self.request('/templates', '{}', 'GET') + self.assertEquals(200, resp.status) + req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + resp = self.request('/templates/test', '{}', 'PUT') + self.assertEquals(401, resp.status) + resp = self.request('/templates/test', '{}', 'DELETE') + self.assertEquals(401, resp.status) + + # Non-root users can only get vms + resp = self.request('/vms', '{}', 'GET') + self.assertEquals(200, resp.status) + resp = self.request('/vms', req, 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/vms', '{}', 'PUT') + self.assertEquals(401, resp.status) + resp = self.request('/vms', '{}', 'DELETE') + self.assertEquals(401, resp.status) diff --git a/tests/utils.py b/tests/utils.py index 14c57d4..18b707c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -147,11 +147,31 @@ def https_request(host, port, path, data=None, method='GET', headers=None): return _request(conn, path, data, method, headers) -def patch_auth(): +def patch_auth(sudo=True): """ Override the authenticate function with a simple test against an internal dict of users and passwords. """ + USER_ID = 'userid' + USER_GROUPS = 'groups' + USER_SUDO = 'sudo' + + class _User(object): + def __init__(self, userid): + self.user = {} + self.user[USER_ID] = userid + self.user[USER_GROUPS] = None + self.user[USER_SUDO] = sudo + + def get_groups(self): + return self.user[USER_GROUPS] + + def has_sudo(self): + return self.user[USER_SUDO] + + def get_user(self): + return self.user + def _authenticate(username, password, service="passwd"): try: return fake_user[username] == password @@ -161,6 +181,7 @@ def patch_auth(): import kimchi.auth kimchi.auth.authenticate = _authenticate + kimchi.auth.User = _User def normalize_xml(xml_str): -- 1.7.10.4

Reviewed-by: Daniel Barboza <danielhb@linux.vnet.ibm.com> On 02/18/2014 03:23 PM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
Non-root users must have restricted access to Kimchi. This patch block non-root urser to: - get or create debug reports; - reboot or shutdown host system; - create, activate/deactivate or delete networks; - create, activate/deactivate or delete storage pools; - update or delete templates; - create, start/stop/connect or delete vms.
It also updates the tests cases to always run as a root user. And add authorization tests to make sure non-root users have restricted access to kimchi.
Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/control/debugreports.py | 2 +- src/kimchi/control/host.py | 2 +- src/kimchi/control/networks.py | 2 +- src/kimchi/control/storagepools.py | 2 +- src/kimchi/control/templates.py | 2 +- src/kimchi/control/vms.py | 2 +- tests/Makefile.am | 1 + tests/test_authorization.py | 124 ++++++++++++++++++++++++++++++++++++ tests/utils.py | 23 ++++++- 9 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 tests/test_authorization.py
diff --git a/src/kimchi/control/debugreports.py b/src/kimchi/control/debugreports.py index 324d826..57dc0f3 100644 --- a/src/kimchi/control/debugreports.py +++ b/src/kimchi/control/debugreports.py @@ -26,7 +26,7 @@ from kimchi.control.utils import internal_redirect from kimchi.control.utils import UrlSubNode
-@UrlSubNode("debugreports", True) +@UrlSubNode("debugreports", True, ['GET', 'POST']) class DebugReports(AsyncCollection): def __init__(self, model): super(DebugReports, self).__init__(model) diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 0852bd0..41e0040 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -31,7 +31,7 @@ from kimchi.exception import OperationFailed from kimchi.template import render
-@UrlSubNode("host", True) +@UrlSubNode("host", True, ['POST']) class Host(Resource): def __init__(self, model, id=None): super(Host, self).__init__(model, id) diff --git a/src/kimchi/control/networks.py b/src/kimchi/control/networks.py index 8510e49..3a02f60 100644 --- a/src/kimchi/control/networks.py +++ b/src/kimchi/control/networks.py @@ -25,7 +25,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.utils import UrlSubNode
-@UrlSubNode("networks", True) +@UrlSubNode("networks", True, ['POST', 'DELETE']) class Networks(Collection): def __init__(self, model): super(Networks, self).__init__(model) diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index ea19609..7e6bdd7 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -34,7 +34,7 @@ from kimchi.model.storagepools import ISO_POOL_NAME from kimchi.control.utils import UrlSubNode
-@UrlSubNode("storagepools", True) +@UrlSubNode("storagepools", True, ['POST', 'DELETE']) class StoragePools(Collection): def __init__(self, model): super(StoragePools, self).__init__(model) diff --git a/src/kimchi/control/templates.py b/src/kimchi/control/templates.py index 58dafcc..8135e32 100644 --- a/src/kimchi/control/templates.py +++ b/src/kimchi/control/templates.py @@ -25,7 +25,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.utils import UrlSubNode
-@UrlSubNode("templates", True) +@UrlSubNode("templates", True, ['PUT', 'DELETE']) class Templates(Collection): def __init__(self, model): super(Templates, self).__init__(model) diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py index 60fc8ff..a74ce27 100644 --- a/src/kimchi/control/vms.py +++ b/src/kimchi/control/vms.py @@ -27,7 +27,7 @@ from kimchi.control.utils import internal_redirect, UrlSubNode from kimchi.control.vm import sub_nodes
-@UrlSubNode("vms", True) +@UrlSubNode("vms", True, ['POST', 'PUT', 'DELETE']) class VMs(Collection): def __init__(self, model): super(VMs, self).__init__(model) diff --git a/tests/Makefile.am b/tests/Makefile.am index 0487ffc..e8db05c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -24,6 +24,7 @@ EXTRA_DIST = \ Makefile.am \ run_tests.sh.in \ iso_gen.py \ + test_authorization.py \ test_config.py.in \ test_exception.py \ test_mockmodel.py \ diff --git a/tests/test_authorization.py b/tests/test_authorization.py new file mode 100644 index 0000000..7f939a8 --- /dev/null +++ b/tests/test_authorization.py @@ -0,0 +1,124 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import json +import os +import unittest + + +from functools import partial + + +import kimchi.mockmodel +from utils import get_free_port, patch_auth, request +from utils import run_server + + +test_server = None +model = None +host = None +port = None +ssl_port = None + + +def setUpModule(): + global test_server, model, host, port, ssl_port + + patch_auth(sudo = False) + model = kimchi.mockmodel.MockModel('/tmp/obj-store-test') + host = '127.0.0.1' + port = get_free_port('http') + test_server = run_server(host, port, None, test_mode=True, model=model) + + +def tearDownModule(): + test_server.stop() + os.unlink('/tmp/obj-store-test') + + +class AuthorizationTests(unittest.TestCase): + def setUp(self): + self.request = partial(request, host, port) + model.reset() + + def test_nonroot_access(self): + # Non-root users can access static host information + resp = self.request('/host', '{}', 'GET') + self.assertEquals(200, resp.status) + + # Non-root users can access host stats + resp = self.request('/host/stats', '{}', 'GET') + self.assertEquals(200, resp.status) + + # Non-root users can not reboot/shutdown host system + resp = self.request('/host/reboot', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/host/shutdown', '{}', 'POST') + self.assertEquals(401, resp.status) + + # Non-root users can not get or debug reports + resp = self.request('/debugreports', '{}', 'GET') + self.assertEquals(401, resp.status) + resp = self.request('/debugreports', '{}', 'POST') + self.assertEquals(401, resp.status) + + # Non-root users can not create or delete network (only get) + resp = self.request('/networks', '{}', 'GET') + self.assertEquals(200, resp.status) + resp = self.request('/networks', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/networks/default/activate', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/networks/default', '{}', 'DELETE') + self.assertEquals(401, resp.status) + + # Non-root users can not create or delete storage pool (only get) + resp = self.request('/storagepools', '{}', 'GET') + self.assertEquals(200, resp.status) + resp = self.request('/storagepools', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/storagepools/default/activate', '{}', 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/storagepools/default', '{}', 'DELETE') + self.assertEquals(401, resp.status) + + # Non-root users can not update or delete a template + # but he can get and create a new one + resp = self.request('/templates', '{}', 'GET') + self.assertEquals(200, resp.status) + req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + resp = self.request('/templates/test', '{}', 'PUT') + self.assertEquals(401, resp.status) + resp = self.request('/templates/test', '{}', 'DELETE') + self.assertEquals(401, resp.status) + + # Non-root users can only get vms + resp = self.request('/vms', '{}', 'GET') + self.assertEquals(200, resp.status) + resp = self.request('/vms', req, 'POST') + self.assertEquals(401, resp.status) + resp = self.request('/vms', '{}', 'PUT') + self.assertEquals(401, resp.status) + resp = self.request('/vms', '{}', 'DELETE') + self.assertEquals(401, resp.status) diff --git a/tests/utils.py b/tests/utils.py index 14c57d4..18b707c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -147,11 +147,31 @@ def https_request(host, port, path, data=None, method='GET', headers=None): return _request(conn, path, data, method, headers)
-def patch_auth(): +def patch_auth(sudo=True): """ Override the authenticate function with a simple test against an internal dict of users and passwords. """ + USER_ID = 'userid' + USER_GROUPS = 'groups' + USER_SUDO = 'sudo' + + class _User(object): + def __init__(self, userid): + self.user = {} + self.user[USER_ID] = userid + self.user[USER_GROUPS] = None + self.user[USER_SUDO] = sudo + + def get_groups(self): + return self.user[USER_GROUPS] + + def has_sudo(self): + return self.user[USER_SUDO] + + def get_user(self): + return self.user + def _authenticate(username, password, service="passwd"): try: return fake_user[username] == password @@ -161,6 +181,7 @@ def patch_auth():
import kimchi.auth kimchi.auth.authenticate = _authenticate + kimchi.auth.User = _User
def normalize_xml(xml_str):
participants (2)
-
Aline Manera
-
Daniel H Barboza