From 757ee34dac0d17669a13504817f51dc70034fba4 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 16 Mar 2016 15:55:07 +0100 Subject: [PATCH] Support auth for network V2 hypervisors --- gns3server/controller/hypervisor.py | 92 +++++++++++++++++++++-------- gns3server/controller/vm.py | 1 - tests/controller/test_hypervisor.py | 73 +++++++++++++++++++++-- 3 files changed, 136 insertions(+), 30 deletions(-) diff --git a/gns3server/controller/hypervisor.py b/gns3server/controller/hypervisor.py index 0910bed7..b1710508 100644 --- a/gns3server/controller/hypervisor.py +++ b/gns3server/controller/hypervisor.py @@ -18,6 +18,7 @@ import aiohttp import asyncio import json +from pkg_resources import parse_version from ..controller.controller_error import ControllerError from ..config import Config @@ -43,8 +44,9 @@ class Hypervisor: self._protocol = protocol self._host = host self._port = port - self._user = user - self._password = password + self._user = None + self._password = None + self._setAuth(user, password) self._connected = False # The remote hypervisor version # TODO: For the moment it's fake we return the controller version @@ -55,6 +57,17 @@ class Hypervisor: if hypervisor_id == "local" and Config.instance().get_section_config("Server")["local"] is False: raise HypervisorError("The local hypervisor is started without --local") + def _setAuth(self, user, password): + """ + Set authentication parameters + """ + self._user = user + self._password = password + if self._user and self._password: + self._auth = aiohttp.BasicAuth(self._user, self._password) + else: + self._auth = None + @property def id(self): """ @@ -69,6 +82,22 @@ class Hypervisor: """ return self._host + @property + def user(self): + return self._user + + @user.setter + def user(self, value): + self._setAuth(value, self._password) + + @property + def password(self): + return self._password + + @user.setter + def password(self, value): + self._setAuth(self._user, value) + def __json__(self): return { "hypervisor_id": self._id, @@ -76,38 +105,55 @@ class Hypervisor: "host": self._host, "port": self._port, "user": self._user, - "connected": self._connected, - "version": self._version + "connected": self._connected } @asyncio.coroutine def httpQuery(self, method, path, data=None): + if not self._connected: + response = yield from self._runHttpQuery("GET", "/version") + if "version" not in response.json: + raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id)) + if parse_version(__version__)[:2] != parse_version(response.json["version"])[:2]: + raise aiohttp.web.HTTPConflict(text="The server {} versions are not compatible {} != {}".format(self._id, __version__, response.json["version"])) + + self._connected = True + return (yield from self._runHttpQuery(method, path, data=data)) + + @asyncio.coroutine + def _runHttpQuery(self, method, path, data=None): with aiohttp.Timeout(10): with aiohttp.ClientSession() as session: url = "{}://{}:{}/v2/hypervisor{}".format(self._protocol, self._host, self._port, path) headers = {'content-type': 'application/json'} - if hasattr(data, '__json__'): - data = data.__json__() - data = json.dumps(data) - response = yield from session.request(method, url, headers=headers, data=data) + if data: + if hasattr(data, '__json__'): + data = data.__json__() + data = json.dumps(data) + response = yield from session.request(method, url, headers=headers, data=data, auth=self._auth) body = yield from response.read() if body: body = body.decode() - if response.status == 400: - raise aiohttp.web.HTTPBadRequest(text=body) - elif response.status == 401: - raise aiohttp.web.HTTPUnauthorized(text=body) - elif response.status == 403: - raise aiohttp.web.HTTPForbidden(text=body) - elif response.status == 404: - raise aiohttp.web.HTTPNotFound(text="{} not found on hypervisor".format(url)) - elif response.status == 409: - raise aiohttp.web.HTTPConflict(text=body) - elif response.status >= 300: - raise NotImplemented("{} status code is not supported".format(e.status)) - if body and len(body): - response.json = json.loads(body) - else: + + if response.status >= 300: + if response.status == 400: + raise aiohttp.web.HTTPBadRequest(text="Bad request {} {}".format(url, body)) + elif response.status == 401: + raise aiohttp.web.HTTPUnauthorized(text="Invalid authentication for hypervisor {}".format(self.id)) + elif response.status == 403: + raise aiohttp.web.HTTPForbidden(text="Forbidden {} {}".format(url, body)) + elif response.status == 404: + raise aiohttp.web.HTTPNotFound(text="{} not found on hypervisor".format(url)) + elif response.status == 409: + raise aiohttp.web.HTTPConflict(text="Conflict {} {}".format(url, body)) + else: + raise NotImplemented("{} status code is not supported".format(e.status)) + if len(body): + try: + response.json = json.loads(body) + except json.JSONDecodeError: + raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server".format(self._id)) + if response.json is None: response.json = {} return response diff --git a/gns3server/controller/vm.py b/gns3server/controller/vm.py index 767cdc79..22d48f5b 100644 --- a/gns3server/controller/vm.py +++ b/gns3server/controller/vm.py @@ -87,7 +87,6 @@ class VM: """ return self._hypervisor.host - @asyncio.coroutine def create(self): data = copy.copy(self._properties) diff --git a/tests/controller/test_hypervisor.py b/tests/controller/test_hypervisor.py index 72035732..23258e9a 100644 --- a/tests/controller/test_hypervisor.py +++ b/tests/controller/test_hypervisor.py @@ -24,12 +24,14 @@ from unittest.mock import patch, MagicMock from gns3server.controller.project import Project from gns3server.controller.hypervisor import Hypervisor, HypervisorError from gns3server.version import __version__ -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, AsyncioMagicMock @pytest.fixture def hypervisor(): - return Hypervisor("my_hypervisor_id", protocol="https", host="example.com", port=84, user="test", password="secure") + hypervisor = Hypervisor("my_hypervisor_id", protocol="https", host="example.com", port=84) + hypervisor._connected = True + return hypervisor def test_init(hypervisor): @@ -56,7 +58,66 @@ def test_hypervisor_httpQuery(hypervisor, async_run): response.status = 200 async_run(hypervisor.post("/projects", {"a": "b"})) - mock.assert_called_with("POST", "https://example.com:84/v2/hypervisor/projects", data='{"a": "b"}', headers={'content-type': 'application/json'}) + mock.assert_called_with("POST", "https://example.com:84/v2/hypervisor/projects", data='{"a": "b"}', headers={'content-type': 'application/json'}, auth=None) + assert hypervisor._auth is None + + +def test_hypervisor_httpQueryAuth(hypervisor, async_run): + response = MagicMock() + with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: + response.status = 200 + + hypervisor.user = "root" + hypervisor.password = "toor" + async_run(hypervisor.post("/projects", {"a": "b"})) + mock.assert_called_with("POST", "https://example.com:84/v2/hypervisor/projects", data='{"a": "b"}', headers={'content-type': 'application/json'}, auth=hypervisor._auth) + assert hypervisor._auth.login == "root" + assert hypervisor._auth.password == "toor" + + +def test_hypervisor_httpQueryNotConnected(hypervisor, async_run): + hypervisor._connected = False + response = AsyncioMagicMock() + response.read = AsyncioMagicMock(return_value = json.dumps({"version": __version__}).encode()) + response.status = 200 + with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: + async_run(hypervisor.post("/projects", {"a": "b"})) + mock.assert_any_call("GET", "https://example.com:84/v2/hypervisor/version", headers={'content-type': 'application/json'}, data=None, auth=None) + mock.assert_any_call("POST", "https://example.com:84/v2/hypervisor/projects", data='{"a": "b"}', headers={'content-type': 'application/json'}, auth=None) + assert hypervisor._connected + + +def test_hypervisor_httpQueryNotConnectedInvalidVersion(hypervisor, async_run): + hypervisor._connected = False + response = AsyncioMagicMock() + response.read = AsyncioMagicMock(return_value = json.dumps({"version": "1.42.4"}).encode()) + response.status = 200 + with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: + with pytest.raises(aiohttp.web.HTTPConflict): + async_run(hypervisor.post("/projects", {"a": "b"})) + mock.assert_any_call("GET", "https://example.com:84/v2/hypervisor/version", headers={'content-type': 'application/json'}, data=None, auth=None) + + +def test_hypervisor_httpQueryNotConnectedNonGNS3Server(hypervisor, async_run): + hypervisor._connected = False + response = AsyncioMagicMock() + response.read = AsyncioMagicMock(return_value = b'Blocked by super antivirus') + response.status = 200 + with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: + with pytest.raises(aiohttp.web.HTTPConflict): + async_run(hypervisor.post("/projects", {"a": "b"})) + mock.assert_any_call("GET", "https://example.com:84/v2/hypervisor/version", headers={'content-type': 'application/json'}, data=None, auth=None) + + +def test_hypervisor_httpQueryNotConnectedNonGNS3Server2(hypervisor, async_run): + hypervisor._connected = False + response = AsyncioMagicMock() + response.read = AsyncioMagicMock(return_value = b'{}') + response.status = 200 + with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: + with pytest.raises(aiohttp.web.HTTPConflict): + async_run(hypervisor.post("/projects", {"a": "b"})) + mock.assert_any_call("GET", "https://example.com:84/v2/hypervisor/version", headers={'content-type': 'application/json'}, data=None, auth=None) def test_hypervisor_httpQueryError(hypervisor, async_run): @@ -75,16 +136,16 @@ def test_hypervisor_httpQuery_project(hypervisor, async_run): project = Project() async_run(hypervisor.post("/projects", project)) - mock.assert_called_with("POST", "https://example.com:84/v2/hypervisor/projects", data=json.dumps(project.__json__()), headers={'content-type': 'application/json'}) + mock.assert_called_with("POST", "https://example.com:84/v2/hypervisor/projects", data=json.dumps(project.__json__()), headers={'content-type': 'application/json'}, auth=None) def test_json(hypervisor): + hypervisor.user = "test" assert hypervisor.__json__() == { "hypervisor_id": "my_hypervisor_id", "protocol": "https", "host": "example.com", "port": 84, "user": "test", - "connected": False, - "version": __version__ + "connected": True }