diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index efabd03b586..0366dfa2b8b 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -32,7 +32,7 @@ ON_DEMAND = ('zwave',) async def async_setup(hass, config): """Set up the config component.""" await hass.components.frontend.async_register_built_in_panel( - 'config', 'config', 'hass:settings') + 'config', 'config', 'hass:settings', require_admin=True) async def setup_panel(panel_name): """Set up a panel.""" diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 30b9d350df6..5a10b60f12f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -123,14 +123,18 @@ class Panel: # Config to pass to the webcomponent config = None + # If the panel should only be visible to admins + require_admin = False + def __init__(self, component_name, sidebar_title, sidebar_icon, - frontend_url_path, config): + frontend_url_path, config, require_admin): """Initialize a built-in panel.""" self.component_name = component_name self.sidebar_title = sidebar_title self.sidebar_icon = sidebar_icon self.frontend_url_path = frontend_url_path or component_name self.config = config + self.require_admin = require_admin @callback def async_register_index_routes(self, router, index_view): @@ -150,16 +154,18 @@ class Panel: 'title': self.sidebar_title, 'config': self.config, 'url_path': self.frontend_url_path, + 'require_admin': self.require_admin, } @bind_hass async def async_register_built_in_panel(hass, component_name, sidebar_title=None, sidebar_icon=None, - frontend_url_path=None, config=None): + frontend_url_path=None, config=None, + require_admin=False): """Register a built-in panel.""" panel = Panel(component_name, sidebar_title, sidebar_icon, - frontend_url_path, config) + frontend_url_path, config, require_admin) panels = hass.data.get(DATA_PANELS) if panels is None: @@ -247,9 +253,11 @@ async def async_setup(hass, config): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( - 'dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')], - loop=hass.loop) + 'kiosk', 'states', 'profile')], loop=hass.loop) + await asyncio.wait( + [async_register_built_in_panel(hass, panel, require_admin=True) + for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', + 'dev-template', 'dev-mqtt')], loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -478,9 +486,11 @@ def websocket_get_panels(hass, connection, msg): Async friendly. """ + user_is_admin = connection.user.is_admin panels = { - panel: connection.hass.data[DATA_PANELS][panel].to_response() - for panel in connection.hass.data[DATA_PANELS]} + panel_key: panel.to_response() + for panel_key, panel in connection.hass.data[DATA_PANELS].items() + if user_is_admin or not panel.require_admin} connection.send_message(websocket_api.result_message( msg['id'], panels)) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7f85c8cfc3f..7e47ac152e3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -189,6 +189,7 @@ async def async_setup(hass, config): sidebar_icon='hass:home-assistant', js_url='/api/hassio/app/entrypoint.js', embed_iframe=True, + require_admin=True, ) await hassio.update_hass_api(config.get('http', {}), refresh_token.token) diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index 2fce5d9857c..7fe2191f4c4 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -23,6 +23,7 @@ CONF_MODULE_URL = 'module_url' CONF_EMBED_IFRAME = 'embed_iframe' CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' CONF_URL_EXCLUSIVE_GROUP = 'url_exclusive_group' +CONF_REQUIRE_ADMIN = 'require_admin' MSG_URL_CONFLICT = \ 'Pass in only one of webcomponent_path, module_url or js_url' @@ -52,6 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ default=DEFAULT_EMBED_IFRAME): cv.boolean, vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=DEFAULT_TRUST_EXTERNAL): cv.boolean, + vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, })]) }, extra=vol.ALLOW_EXTRA) @@ -77,7 +79,9 @@ async def async_register_panel( # Should user be asked for confirmation when loading external source trust_external=DEFAULT_TRUST_EXTERNAL, # Configuration to be passed to the panel - config=None): + config=None, + # If your panel should only be shown to admin users + require_admin=False): """Register a new custom panel.""" if js_url is None and html_url is None and module_url is None: raise ValueError('Either js_url, module_url or html_url is required.') @@ -115,7 +119,8 @@ async def async_register_panel( sidebar_title=sidebar_title, sidebar_icon=sidebar_icon, frontend_url_path=frontend_url_path, - config=config + config=config, + require_admin=require_admin, ) @@ -134,6 +139,7 @@ async def async_setup(hass, config): 'config': panel.get(CONF_CONFIG), 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], 'embed_iframe': panel[CONF_EMBED_IFRAME], + 'require_admin': panel[CONF_REQUIRE_ADMIN], } panel_path = panel.get(CONF_WEBCOMPONENT_PATH) diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py index b82f9fa9789..9319dfcc6ad 100644 --- a/homeassistant/components/panel_iframe/__init__.py +++ b/homeassistant/components/panel_iframe/__init__.py @@ -12,6 +12,7 @@ CONF_TITLE = 'title' CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required." CONF_RELATIVE_URL_REGEX = r'\A/' +CONF_REQUIRE_ADMIN = 'require_admin' CONFIG_SCHEMA = vol.Schema({ DOMAIN: cv.schema_with_slug_keys( @@ -19,6 +20,7 @@ CONFIG_SCHEMA = vol.Schema({ # pylint: disable=no-value-for-parameter vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, vol.Required(CONF_URL): vol.Any( vol.Match( CONF_RELATIVE_URL_REGEX, @@ -34,6 +36,7 @@ async def async_setup(hass, config): for url_path, info in config[DOMAIN].items(): await hass.components.frontend.async_register_built_in_panel( 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), - url_path, {'url': info[CONF_URL]}) + url_path, {'url': info[CONF_URL]}, + require_admin=info[CONF_REQUIRE_ADMIN]) return True diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index b1b9a70d594..e4ed2c15ecb 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -249,7 +249,7 @@ async def test_get_panels(hass, hass_ws_client): """Test get_panels command.""" await async_setup_component(hass, 'frontend') await hass.components.frontend.async_register_built_in_panel( - 'map', 'Map', 'mdi:tooltip-account') + 'map', 'Map', 'mdi:tooltip-account', require_admin=True) client = await hass_ws_client(hass) await client.send_json({ @@ -266,6 +266,31 @@ async def test_get_panels(hass, hass_ws_client): assert msg['result']['map']['url_path'] == 'map' assert msg['result']['map']['icon'] == 'mdi:tooltip-account' assert msg['result']['map']['title'] == 'Map' + assert msg['result']['map']['require_admin'] is True + + +async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user): + """Test get_panels command.""" + hass_admin_user.groups = [] + await async_setup_component(hass, 'frontend') + await hass.components.frontend.async_register_built_in_panel( + 'map', 'Map', 'mdi:tooltip-account', require_admin=True) + await hass.components.frontend.async_register_built_in_panel( + 'history', 'History', 'mdi:history') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'get_panels', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] + assert 'history' in msg['result'] + assert 'map' not in msg['result'] async def test_get_translations(hass, hass_ws_client): diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 1326805fc93..ba642b698f7 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,6 +8,7 @@ import pytest from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.setup import async_setup_component from homeassistant.components.hassio import STORAGE_KEY +from homeassistant.components import frontend from tests.common import mock_coro @@ -44,6 +45,28 @@ def test_setup_api_ping(hass, aioclient_mock): assert hass.components.hassio.is_hassio() +async def test_setup_api_panel(hass, aioclient_mock): + """Test setup with API ping.""" + assert await async_setup_component(hass, 'frontend', {}) + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component(hass, 'hassio', {}) + assert result + + panels = hass.data[frontend.DATA_PANELS] + + assert panels.get('hassio').to_response() == { + 'component_name': 'custom', + 'icon': 'hass:home-assistant', + 'title': 'Hass.io', + 'url_path': 'hassio', + 'require_admin': True, + 'config': {'_panel_custom': {'embed_iframe': True, + 'js_url': '/api/hassio/app/entrypoint.js', + 'name': 'hassio-main', + 'trust_external': False}}, + } + + @asyncio.coroutine def test_setup_api_push_api_data(hass, aioclient_mock): """Test setup with API push.""" diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index c265324179d..8c95f96085a 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -130,6 +130,7 @@ async def test_module_webcomponent(hass): }, 'embed_iframe': True, 'trust_external_script': True, + 'require_admin': True, } } @@ -145,6 +146,7 @@ async def test_module_webcomponent(hass): panel = panels['nice_url'] + assert panel.require_admin assert panel.config == { 'hello': 'world', '_panel_custom': { diff --git a/tests/components/panel_iframe/test_init.py b/tests/components/panel_iframe/test_init.py index cb868f64b58..da7878399d4 100644 --- a/tests/components/panel_iframe/test_init.py +++ b/tests/components/panel_iframe/test_init.py @@ -41,11 +41,13 @@ class TestPanelIframe(unittest.TestCase): 'icon': 'mdi:network-wireless', 'title': 'Router', 'url': 'http://192.168.1.1', + 'require_admin': True, }, 'weather': { 'icon': 'mdi:weather', 'title': 'Weather', 'url': 'https://www.wunderground.com/us/ca/san-diego', + 'require_admin': True, }, 'api': { 'icon': 'mdi:weather', @@ -67,7 +69,8 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', 'title': 'Router', - 'url_path': 'router' + 'url_path': 'router', + 'require_admin': True, } assert panels.get('weather').to_response() == { @@ -76,6 +79,7 @@ class TestPanelIframe(unittest.TestCase): 'icon': 'mdi:weather', 'title': 'Weather', 'url_path': 'weather', + 'require_admin': True, } assert panels.get('api').to_response() == { @@ -84,6 +88,7 @@ class TestPanelIframe(unittest.TestCase): 'icon': 'mdi:weather', 'title': 'Api', 'url_path': 'api', + 'require_admin': False, } assert panels.get('ftp').to_response() == { @@ -92,4 +97,5 @@ class TestPanelIframe(unittest.TestCase): 'icon': 'mdi:weather', 'title': 'FTP', 'url_path': 'ftp', + 'require_admin': False, }