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
This commit is contained in:
parent
b57d809dad
commit
f1a0ad9e4a
9 changed files with 90 additions and 14 deletions
|
@ -32,7 +32,7 @@ ON_DEMAND = ('zwave',)
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the config component."""
|
"""Set up the config component."""
|
||||||
await hass.components.frontend.async_register_built_in_panel(
|
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):
|
async def setup_panel(panel_name):
|
||||||
"""Set up a panel."""
|
"""Set up a panel."""
|
||||||
|
|
|
@ -123,14 +123,18 @@ class Panel:
|
||||||
# Config to pass to the webcomponent
|
# Config to pass to the webcomponent
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
|
# If the panel should only be visible to admins
|
||||||
|
require_admin = False
|
||||||
|
|
||||||
def __init__(self, component_name, sidebar_title, sidebar_icon,
|
def __init__(self, component_name, sidebar_title, sidebar_icon,
|
||||||
frontend_url_path, config):
|
frontend_url_path, config, require_admin):
|
||||||
"""Initialize a built-in panel."""
|
"""Initialize a built-in panel."""
|
||||||
self.component_name = component_name
|
self.component_name = component_name
|
||||||
self.sidebar_title = sidebar_title
|
self.sidebar_title = sidebar_title
|
||||||
self.sidebar_icon = sidebar_icon
|
self.sidebar_icon = sidebar_icon
|
||||||
self.frontend_url_path = frontend_url_path or component_name
|
self.frontend_url_path = frontend_url_path or component_name
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.require_admin = require_admin
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_index_routes(self, router, index_view):
|
def async_register_index_routes(self, router, index_view):
|
||||||
|
@ -150,16 +154,18 @@ class Panel:
|
||||||
'title': self.sidebar_title,
|
'title': self.sidebar_title,
|
||||||
'config': self.config,
|
'config': self.config,
|
||||||
'url_path': self.frontend_url_path,
|
'url_path': self.frontend_url_path,
|
||||||
|
'require_admin': self.require_admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def async_register_built_in_panel(hass, component_name,
|
async def async_register_built_in_panel(hass, component_name,
|
||||||
sidebar_title=None, sidebar_icon=None,
|
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."""
|
"""Register a built-in panel."""
|
||||||
panel = Panel(component_name, sidebar_title, sidebar_icon,
|
panel = Panel(component_name, sidebar_title, sidebar_icon,
|
||||||
frontend_url_path, config)
|
frontend_url_path, config, require_admin)
|
||||||
|
|
||||||
panels = hass.data.get(DATA_PANELS)
|
panels = hass.data.get(DATA_PANELS)
|
||||||
if panels is None:
|
if panels is None:
|
||||||
|
@ -247,9 +253,11 @@ async def async_setup(hass, config):
|
||||||
|
|
||||||
await asyncio.wait(
|
await asyncio.wait(
|
||||||
[async_register_built_in_panel(hass, panel) for panel in (
|
[async_register_built_in_panel(hass, panel) for panel in (
|
||||||
'dev-event', 'dev-info', 'dev-service', 'dev-state',
|
'kiosk', 'states', 'profile')], loop=hass.loop)
|
||||||
'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')],
|
await asyncio.wait(
|
||||||
loop=hass.loop)
|
[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
|
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
|
||||||
|
|
||||||
|
@ -478,9 +486,11 @@ def websocket_get_panels(hass, connection, msg):
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
|
user_is_admin = connection.user.is_admin
|
||||||
panels = {
|
panels = {
|
||||||
panel: connection.hass.data[DATA_PANELS][panel].to_response()
|
panel_key: panel.to_response()
|
||||||
for panel in connection.hass.data[DATA_PANELS]}
|
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(
|
connection.send_message(websocket_api.result_message(
|
||||||
msg['id'], panels))
|
msg['id'], panels))
|
||||||
|
|
|
@ -189,6 +189,7 @@ async def async_setup(hass, config):
|
||||||
sidebar_icon='hass:home-assistant',
|
sidebar_icon='hass:home-assistant',
|
||||||
js_url='/api/hassio/app/entrypoint.js',
|
js_url='/api/hassio/app/entrypoint.js',
|
||||||
embed_iframe=True,
|
embed_iframe=True,
|
||||||
|
require_admin=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
await hassio.update_hass_api(config.get('http', {}), refresh_token.token)
|
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_EMBED_IFRAME = 'embed_iframe'
|
||||||
CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script'
|
CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script'
|
||||||
CONF_URL_EXCLUSIVE_GROUP = 'url_exclusive_group'
|
CONF_URL_EXCLUSIVE_GROUP = 'url_exclusive_group'
|
||||||
|
CONF_REQUIRE_ADMIN = 'require_admin'
|
||||||
|
|
||||||
MSG_URL_CONFLICT = \
|
MSG_URL_CONFLICT = \
|
||||||
'Pass in only one of webcomponent_path, module_url or js_url'
|
'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,
|
default=DEFAULT_EMBED_IFRAME): cv.boolean,
|
||||||
vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT,
|
vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT,
|
||||||
default=DEFAULT_TRUST_EXTERNAL): cv.boolean,
|
default=DEFAULT_TRUST_EXTERNAL): cv.boolean,
|
||||||
|
vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean,
|
||||||
})])
|
})])
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
@ -77,7 +79,9 @@ async def async_register_panel(
|
||||||
# Should user be asked for confirmation when loading external source
|
# Should user be asked for confirmation when loading external source
|
||||||
trust_external=DEFAULT_TRUST_EXTERNAL,
|
trust_external=DEFAULT_TRUST_EXTERNAL,
|
||||||
# Configuration to be passed to the panel
|
# 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."""
|
"""Register a new custom panel."""
|
||||||
if js_url is None and html_url is None and module_url is None:
|
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.')
|
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_title=sidebar_title,
|
||||||
sidebar_icon=sidebar_icon,
|
sidebar_icon=sidebar_icon,
|
||||||
frontend_url_path=frontend_url_path,
|
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),
|
'config': panel.get(CONF_CONFIG),
|
||||||
'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
|
'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
|
||||||
'embed_iframe': panel[CONF_EMBED_IFRAME],
|
'embed_iframe': panel[CONF_EMBED_IFRAME],
|
||||||
|
'require_admin': panel[CONF_REQUIRE_ADMIN],
|
||||||
}
|
}
|
||||||
|
|
||||||
panel_path = panel.get(CONF_WEBCOMPONENT_PATH)
|
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_ERROR_MSG = "Invalid relative URL. Absolute path required."
|
||||||
CONF_RELATIVE_URL_REGEX = r'\A/'
|
CONF_RELATIVE_URL_REGEX = r'\A/'
|
||||||
|
CONF_REQUIRE_ADMIN = 'require_admin'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: cv.schema_with_slug_keys(
|
DOMAIN: cv.schema_with_slug_keys(
|
||||||
|
@ -19,6 +20,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
vol.Optional(CONF_TITLE): cv.string,
|
vol.Optional(CONF_TITLE): cv.string,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
|
vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean,
|
||||||
vol.Required(CONF_URL): vol.Any(
|
vol.Required(CONF_URL): vol.Any(
|
||||||
vol.Match(
|
vol.Match(
|
||||||
CONF_RELATIVE_URL_REGEX,
|
CONF_RELATIVE_URL_REGEX,
|
||||||
|
@ -34,6 +36,7 @@ async def async_setup(hass, config):
|
||||||
for url_path, info in config[DOMAIN].items():
|
for url_path, info in config[DOMAIN].items():
|
||||||
await hass.components.frontend.async_register_built_in_panel(
|
await hass.components.frontend.async_register_built_in_panel(
|
||||||
'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
|
'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
|
return True
|
||||||
|
|
|
@ -249,7 +249,7 @@ async def test_get_panels(hass, hass_ws_client):
|
||||||
"""Test get_panels command."""
|
"""Test get_panels command."""
|
||||||
await async_setup_component(hass, 'frontend')
|
await async_setup_component(hass, 'frontend')
|
||||||
await hass.components.frontend.async_register_built_in_panel(
|
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)
|
client = await hass_ws_client(hass)
|
||||||
await client.send_json({
|
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']['url_path'] == 'map'
|
||||||
assert msg['result']['map']['icon'] == 'mdi:tooltip-account'
|
assert msg['result']['map']['icon'] == 'mdi:tooltip-account'
|
||||||
assert msg['result']['map']['title'] == 'Map'
|
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):
|
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.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components.hassio import STORAGE_KEY
|
from homeassistant.components.hassio import STORAGE_KEY
|
||||||
|
from homeassistant.components import frontend
|
||||||
|
|
||||||
from tests.common import mock_coro
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
@ -44,6 +45,28 @@ def test_setup_api_ping(hass, aioclient_mock):
|
||||||
assert hass.components.hassio.is_hassio()
|
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
|
@asyncio.coroutine
|
||||||
def test_setup_api_push_api_data(hass, aioclient_mock):
|
def test_setup_api_push_api_data(hass, aioclient_mock):
|
||||||
"""Test setup with API push."""
|
"""Test setup with API push."""
|
||||||
|
|
|
@ -130,6 +130,7 @@ async def test_module_webcomponent(hass):
|
||||||
},
|
},
|
||||||
'embed_iframe': True,
|
'embed_iframe': True,
|
||||||
'trust_external_script': True,
|
'trust_external_script': True,
|
||||||
|
'require_admin': True,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +146,7 @@ async def test_module_webcomponent(hass):
|
||||||
|
|
||||||
panel = panels['nice_url']
|
panel = panels['nice_url']
|
||||||
|
|
||||||
|
assert panel.require_admin
|
||||||
assert panel.config == {
|
assert panel.config == {
|
||||||
'hello': 'world',
|
'hello': 'world',
|
||||||
'_panel_custom': {
|
'_panel_custom': {
|
||||||
|
|
|
@ -41,11 +41,13 @@ class TestPanelIframe(unittest.TestCase):
|
||||||
'icon': 'mdi:network-wireless',
|
'icon': 'mdi:network-wireless',
|
||||||
'title': 'Router',
|
'title': 'Router',
|
||||||
'url': 'http://192.168.1.1',
|
'url': 'http://192.168.1.1',
|
||||||
|
'require_admin': True,
|
||||||
},
|
},
|
||||||
'weather': {
|
'weather': {
|
||||||
'icon': 'mdi:weather',
|
'icon': 'mdi:weather',
|
||||||
'title': 'Weather',
|
'title': 'Weather',
|
||||||
'url': 'https://www.wunderground.com/us/ca/san-diego',
|
'url': 'https://www.wunderground.com/us/ca/san-diego',
|
||||||
|
'require_admin': True,
|
||||||
},
|
},
|
||||||
'api': {
|
'api': {
|
||||||
'icon': 'mdi:weather',
|
'icon': 'mdi:weather',
|
||||||
|
@ -67,7 +69,8 @@ class TestPanelIframe(unittest.TestCase):
|
||||||
'config': {'url': 'http://192.168.1.1'},
|
'config': {'url': 'http://192.168.1.1'},
|
||||||
'icon': 'mdi:network-wireless',
|
'icon': 'mdi:network-wireless',
|
||||||
'title': 'Router',
|
'title': 'Router',
|
||||||
'url_path': 'router'
|
'url_path': 'router',
|
||||||
|
'require_admin': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert panels.get('weather').to_response() == {
|
assert panels.get('weather').to_response() == {
|
||||||
|
@ -76,6 +79,7 @@ class TestPanelIframe(unittest.TestCase):
|
||||||
'icon': 'mdi:weather',
|
'icon': 'mdi:weather',
|
||||||
'title': 'Weather',
|
'title': 'Weather',
|
||||||
'url_path': 'weather',
|
'url_path': 'weather',
|
||||||
|
'require_admin': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert panels.get('api').to_response() == {
|
assert panels.get('api').to_response() == {
|
||||||
|
@ -84,6 +88,7 @@ class TestPanelIframe(unittest.TestCase):
|
||||||
'icon': 'mdi:weather',
|
'icon': 'mdi:weather',
|
||||||
'title': 'Api',
|
'title': 'Api',
|
||||||
'url_path': 'api',
|
'url_path': 'api',
|
||||||
|
'require_admin': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert panels.get('ftp').to_response() == {
|
assert panels.get('ftp').to_response() == {
|
||||||
|
@ -92,4 +97,5 @@ class TestPanelIframe(unittest.TestCase):
|
||||||
'icon': 'mdi:weather',
|
'icon': 'mdi:weather',
|
||||||
'title': 'FTP',
|
'title': 'FTP',
|
||||||
'url_path': 'ftp',
|
'url_path': 'ftp',
|
||||||
|
'require_admin': False,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue