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 flake8pull/22398/head
parent
b57d809dad
commit
f1a0ad9e4a
|
@ -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."""
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue