From e19976b4856b08fcb874215aeb18e85dbaca402a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Jul 2018 14:32:33 +0200 Subject: [PATCH 1/2] Hass.io sent token to supervisor --- homeassistant/components/hassio/__init__.py | 26 +++- homeassistant/components/hassio/handler.py | 21 ++- tests/components/hassio/test_init.py | 136 +++++++++++--------- 3 files changed, 111 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6ab86435371f1b..7f86db9146020c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -27,6 +27,8 @@ DOMAIN = 'hassio' DEPENDENCIES = ['http'] +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 CONF_FRONTEND_REPO = 'development_repo' @@ -167,6 +169,21 @@ def async_setup(hass, config): _LOGGER.error("Not connected with Hass.io") return False + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + data = yield from store.async_load() + + if data is None: + data = {} + + if 'hassio_user' in data: + user = yield from hass.auth.async_get_user(data['hassio_user']) + refresh_token = list(user.refresh_tokens.values())[0] + else: + user = yield from hass.auth.async_create_system_user('Hass.io') + refresh_token = yield from hass.auth.async_create_refresh_token(user) + data['hassio_user'] = user.id + yield from store.async_save(data) + # This overrides the normal API call that would be forwarded development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) if development_repo is not None: @@ -186,8 +203,13 @@ def async_setup(hass, config): embed_iframe=True, ) - if 'http' in config: - yield from hassio.update_hass_api(config['http']) + # Temporary. No refresh token tells supervisor to use API password. + if hass.auth.active: + token = refresh_token.token + else: + token = None + + yield from hassio.update_hass_api(config.get('http', {}), token) if 'homeassistant' in config: yield from hassio.update_hass_timezone(config['homeassistant']) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index c3caf40ba62a70..f68818e39881b1 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -23,10 +23,9 @@ def _api_bool(funct): """Return a boolean.""" - @asyncio.coroutine - def _wrapper(*argv, **kwargs): + async def _wrapper(*argv, **kwargs): """Wrap function.""" - data = yield from funct(*argv, **kwargs) + data = await funct(*argv, **kwargs) return data and data['result'] == "ok" return _wrapper @@ -34,10 +33,9 @@ def _wrapper(*argv, **kwargs): def _api_data(funct): """Return data of an api.""" - @asyncio.coroutine - def _wrapper(*argv, **kwargs): + async def _wrapper(*argv, **kwargs): """Wrap function.""" - data = yield from funct(*argv, **kwargs) + data = await funct(*argv, **kwargs) if data and data['result'] == "ok": return data['data'] return None @@ -94,24 +92,23 @@ def check_homeassistant_config(self): return self.send_command("/homeassistant/check", timeout=300) @_api_bool - def update_hass_api(self, http_config): - """Update Home Assistant API data on Hass.io. - - This method return a coroutine. - """ + async def update_hass_api(self, http_config, refresh_token): + """Update Home Assistant API data on Hass.io.""" port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT options = { 'ssl': CONF_SSL_CERTIFICATE in http_config, 'port': port, 'password': http_config.get(CONF_API_PASSWORD), 'watchdog': True, + 'refresh_token': refresh_token, } if CONF_SERVER_HOST in http_config: options['watchdog'] = False _LOGGER.warning("Don't use 'server_host' options with Hass.io") - return self.send_command("/homeassistant/options", payload=options) + return await self.send_command("/homeassistant/options", + payload=options) @_api_bool def update_hass_timezone(self, core_config): diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f67a6cbccec48a..dc95b820d820ba 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -3,8 +3,11 @@ import os from unittest.mock import patch, Mock +import pytest + from homeassistant.setup import async_setup_component -from homeassistant.components.hassio import async_check_config +from homeassistant.components.hassio import ( + STORAGE_KEY, async_check_config) from tests.common import mock_coro @@ -14,21 +17,27 @@ 'HASSIO_TOKEN': 'abcdefgh', } - -@asyncio.coroutine -def test_setup_api_ping(hass, aioclient_mock): - """Test setup with API ping.""" +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock): + """Mock all setup requests.""" + aioclient_mock.post( + "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) +@asyncio.coroutine +def test_setup_api_ping(hass, aioclient_mock): + """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', {}) assert result - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 assert hass.components.hassio.get_homeassistant_version() == "10.0" assert hass.components.hassio.is_hassio() @@ -36,14 +45,6 @@ def test_setup_api_ping(hass, aioclient_mock): @asyncio.coroutine def test_setup_api_push_api_data(hass, aioclient_mock): """Test setup with API push.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { @@ -64,14 +65,6 @@ def test_setup_api_push_api_data(hass, aioclient_mock): @asyncio.coroutine def test_setup_api_push_api_data_server_host(hass, aioclient_mock): """Test setup with API push with active server host.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { @@ -90,19 +83,61 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): assert not aioclient_mock.mock_calls[1][2]['watchdog'] -@asyncio.coroutine -def test_setup_api_push_api_data_default(hass, aioclient_mock): +async def test_setup_api_push_api_data_default(hass, aioclient_mock, + hass_storage): """Test setup with API push default data.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) + with patch.dict(os.environ, MOCK_ENVIRON), \ + patch('homeassistant.auth.AuthManager.active', return_value=True): + result = await async_setup_component(hass, 'hassio', { + 'http': {}, + 'hassio': {} + }) + assert result + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] is None + assert aioclient_mock.mock_calls[1][2]['port'] == 8123 + refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token'] + hassio_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]['data']['hassio_user'] + ) + assert hassio_user is not None + assert hassio_user.system_generated + assert refresh_token in hassio_user.refresh_tokens + + +async def test_setup_api_push_api_data_no_auth(hass, aioclient_mock, + hass_storage): + """Test setup with API push default data.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component(hass, 'hassio', { + result = await async_setup_component(hass, 'hassio', { + 'http': {}, + 'hassio': {} + }) + assert result + + assert aioclient_mock.call_count == 3 + assert not aioclient_mock.mock_calls[1][2]['ssl'] + assert aioclient_mock.mock_calls[1][2]['password'] is None + assert aioclient_mock.mock_calls[1][2]['port'] == 8123 + assert aioclient_mock.mock_calls[1][2]['refresh_token'] is None + + +async def test_setup_api_existing_hassio_user(hass, aioclient_mock, + hass_storage): + """Test setup with API push default data.""" + user = await hass.auth.async_create_system_user('Hass.io test') + token = await hass.auth.async_create_refresh_token(user) + hass_storage[STORAGE_KEY] = { + 'version': 1, + 'data': { + 'hassio_user': user.id + } + } + with patch.dict(os.environ, MOCK_ENVIRON), \ + patch('homeassistant.auth.AuthManager.active', return_value=True): + result = await async_setup_component(hass, 'hassio', { 'http': {}, 'hassio': {} }) @@ -112,19 +147,12 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): assert not aioclient_mock.mock_calls[1][2]['ssl'] assert aioclient_mock.mock_calls[1][2]['password'] is None assert aioclient_mock.mock_calls[1][2]['port'] == 8123 + assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token @asyncio.coroutine def test_setup_core_push_timezone(hass, aioclient_mock): """Test setup with API push default data.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.post( - "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, @@ -134,21 +162,13 @@ def test_setup_core_push_timezone(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 3 - assert aioclient_mock.mock_calls[1][2]['timezone'] == "testzone" + assert aioclient_mock.call_count == 4 + assert aioclient_mock.mock_calls[2][2]['timezone'] == "testzone" @asyncio.coroutine def test_setup_hassio_no_additional_data(hass, aioclient_mock): """Test setup with API push default data.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={ - 'result': 'ok', 'data': {'last_version': '10.0'}}) - aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={'result': 'ok'}) - with patch.dict(os.environ, MOCK_ENVIRON), \ patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}): result = yield from async_setup_component(hass, 'hassio', { @@ -156,7 +176,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456" @@ -234,14 +254,14 @@ def test_service_calls(hassio_env, hass, aioclient_mock): 'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'}) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 4 + assert aioclient_mock.call_count == 5 assert aioclient_mock.mock_calls[-1][2] == 'test' yield from hass.services.async_call('hassio', 'host_shutdown', {}) yield from hass.services.async_call('hassio', 'host_reboot', {}) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 yield from hass.services.async_call('hassio', 'snapshot_full', {}) yield from hass.services.async_call('hassio', 'snapshot_partial', { @@ -251,7 +271,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): }) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 8 + assert aioclient_mock.call_count == 9 assert aioclient_mock.mock_calls[-1][2] == { 'addons': ['test'], 'folders': ['ssl'], 'password': "123456"} @@ -267,7 +287,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): }) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 11 assert aioclient_mock.mock_calls[-1][2] == { 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False, 'password': "123456" @@ -289,17 +309,17 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock): yield from hass.services.async_call('homeassistant', 'stop') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 yield from hass.services.async_call('homeassistant', 'check_config') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 yield from hass.services.async_call('homeassistant', 'restart') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 4 + assert aioclient_mock.call_count == 5 @asyncio.coroutine From a3e6b15e572b05f0a06027ebb41319d5b38fc730 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Jul 2018 15:14:29 +0200 Subject: [PATCH 2/2] Lint --- tests/components/hassio/test_init.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index dc95b820d820ba..b19756697311e0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -17,6 +17,7 @@ 'HASSIO_TOKEN': 'abcdefgh', } + @pytest.fixture(autouse=True) def mock_all(aioclient_mock): """Mock all setup requests.""" @@ -30,6 +31,7 @@ def mock_all(aioclient_mock): "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) + @asyncio.coroutine def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping."""