Frontend indicate require admin (#22272)

* Allow panels to indicate they are meant for admins

* Panels to indicate when they require admin access

* Do not return admin-only panels to non-admin users

* Fix flake8
pull/22398/head
Paulus Schoutsen 2019-03-25 10:04:35 -07:00 committed by GitHub
parent b57d809dad
commit f1a0ad9e4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 90 additions and 14 deletions

View File

@ -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."""

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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."""

View File

@ -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': {

View File

@ -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,
}