Check admin permission before able to manage config entries
parent
6766d25e62
commit
90df932fe1
|
@ -1,5 +1,6 @@
|
|||
"""Permission constants."""
|
||||
CAT_ENTITIES = 'entities'
|
||||
CAT_CONFIG_ENTRIES = 'config_entries'
|
||||
SUBCAT_ALL = 'all'
|
||||
|
||||
POLICY_READ = 'read'
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Http views to control the config manager."""
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
|
||||
|
@ -63,6 +65,9 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
|
|||
|
||||
async def delete(self, request, entry_id):
|
||||
"""Delete a config entry."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(config_entry_id=entry_id, permission='remove')
|
||||
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
|
@ -85,12 +90,26 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
|
|||
Example of a non-user initiated flow is a discovered Hue hub that
|
||||
requires user interaction to finish setup.
|
||||
"""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json([
|
||||
flw for flw in hass.config_entries.flow.async_progress()
|
||||
if flw['context']['source'] != config_entries.SOURCE_USER])
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def post(self, request):
|
||||
"""Handle a POST request."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
|
||||
class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
@ -98,6 +117,24 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
|||
url = '/api/config/config_entries/flow/{flow_id}'
|
||||
name = 'api:config:config_entries:flow:resource'
|
||||
|
||||
async def get(self, request, flow_id):
|
||||
"""Get the current state of a data_entry_flow."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
return await super().get(request, flow_id)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def post(self, request, flow_id):
|
||||
"""Handle a POST request."""
|
||||
if not request['hass_user'].is_admin:
|
||||
raise Unauthorized(
|
||||
perm_category=CAT_CONFIG_ENTRIES, permission='add')
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request, flow_id)
|
||||
|
||||
|
||||
class ConfigManagerAvailableFlowView(HomeAssistantView):
|
||||
"""View to query available flows."""
|
||||
|
|
|
@ -47,12 +47,18 @@ class Unauthorized(HomeAssistantError):
|
|||
def __init__(self, context: Optional['Context'] = None,
|
||||
user_id: Optional[str] = None,
|
||||
entity_id: Optional[str] = None,
|
||||
config_entry_id: Optional[str] = None,
|
||||
perm_category: Optional[str] = None,
|
||||
permission: Optional[Tuple[str]] = None) -> None:
|
||||
"""Unauthorized error."""
|
||||
super().__init__(self.__class__.__name__)
|
||||
self.context = context
|
||||
self.user_id = user_id
|
||||
self.entity_id = entity_id
|
||||
self.config_entry_id = config_entry_id
|
||||
# Not all actions have an ID (like adding config entry)
|
||||
# We then use this fallback to know what category was unauth
|
||||
self.perm_category = perm_category
|
||||
self.permission = permission
|
||||
|
||||
|
||||
|
|
|
@ -84,6 +84,17 @@ def test_remove_entry(hass, client):
|
|||
assert len(hass.config_entries.async_entries()) == 0
|
||||
|
||||
|
||||
async def test_remove_entry_unauth(hass, client, hass_admin_user):
|
||||
"""Test removing an entry via the API."""
|
||||
hass_admin_user.groups = []
|
||||
entry = MockConfigEntry(domain='demo', state=core_ce.ENTRY_STATE_LOADED)
|
||||
entry.add_to_hass(hass)
|
||||
resp = await client.delete(
|
||||
'/api/config/config_entries/entry/{}'.format(entry.entry_id))
|
||||
assert resp.status == 401
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_available_flows(hass, client):
|
||||
"""Test querying the available flows."""
|
||||
|
@ -155,6 +166,35 @@ def test_initialize_flow(hass, client):
|
|||
}
|
||||
|
||||
|
||||
async def test_initialize_flow_unauth(hass, client, hass_admin_user):
|
||||
"""Test we can initialize a flow."""
|
||||
hass_admin_user.groups = []
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
@asyncio.coroutine
|
||||
def async_step_user(self, user_input=None):
|
||||
schema = OrderedDict()
|
||||
schema[vol.Required('username')] = str
|
||||
schema[vol.Required('password')] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=schema,
|
||||
description_placeholders={
|
||||
'url': 'https://example.com',
|
||||
},
|
||||
errors={
|
||||
'username': 'Should be unique.'
|
||||
}
|
||||
)
|
||||
|
||||
with patch.dict(HANDLERS, {'test': TestFlow}):
|
||||
resp = await client.post('/api/config/config_entries/flow',
|
||||
json={'handler': 'test'})
|
||||
|
||||
assert resp.status == 401
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_abort(hass, client):
|
||||
"""Test a flow that aborts."""
|
||||
|
@ -273,6 +313,58 @@ def test_two_step_flow(hass, client):
|
|||
}
|
||||
|
||||
|
||||
async def test_continue_flow_unauth(hass, client, hass_admin_user):
|
||||
"""Test we can't finish a two step flow."""
|
||||
set_component(
|
||||
hass, 'test',
|
||||
MockModule('test', async_setup_entry=mock_coro_func(True)))
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
VERSION = 1
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id='account',
|
||||
data_schema=vol.Schema({
|
||||
'user_title': str
|
||||
}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_account(self, user_input=None):
|
||||
return self.async_create_entry(
|
||||
title=user_input['user_title'],
|
||||
data={'secret': 'account_token'},
|
||||
)
|
||||
|
||||
with patch.dict(HANDLERS, {'test': TestFlow}):
|
||||
resp = await client.post('/api/config/config_entries/flow',
|
||||
json={'handler': 'test'})
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
flow_id = data.pop('flow_id')
|
||||
assert data == {
|
||||
'type': 'form',
|
||||
'handler': 'test',
|
||||
'step_id': 'account',
|
||||
'data_schema': [
|
||||
{
|
||||
'name': 'user_title',
|
||||
'type': 'string'
|
||||
}
|
||||
],
|
||||
'description_placeholders': None,
|
||||
'errors': None
|
||||
}
|
||||
|
||||
hass_admin_user.groups = []
|
||||
|
||||
resp = await client.post(
|
||||
'/api/config/config_entries/flow/{}'.format(flow_id),
|
||||
json={'user_title': 'user-title'})
|
||||
assert resp.status == 401
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_get_progress_index(hass, client):
|
||||
"""Test querying for the flows that are in progress."""
|
||||
|
@ -305,6 +397,29 @@ def test_get_progress_index(hass, client):
|
|||
]
|
||||
|
||||
|
||||
async def test_get_progress_index_unauth(hass, client, hass_admin_user):
|
||||
"""Test we can't get flows that are in progress."""
|
||||
hass_admin_user.groups = []
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
VERSION = 5
|
||||
|
||||
async def async_step_hassio(self, info):
|
||||
return (await self.async_step_account())
|
||||
|
||||
async def async_step_account(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id='account',
|
||||
)
|
||||
|
||||
with patch.dict(HANDLERS, {'test': TestFlow}):
|
||||
form = await hass.config_entries.flow.async_init(
|
||||
'test', context={'source': 'hassio'})
|
||||
|
||||
resp = await client.get('/api/config/config_entries/flow')
|
||||
assert resp.status == 401
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_get_progress_flow(hass, client):
|
||||
"""Test we can query the API for same result as we get from init a flow."""
|
||||
|
@ -337,3 +452,34 @@ def test_get_progress_flow(hass, client):
|
|||
data2 = yield from resp2.json()
|
||||
|
||||
assert data == data2
|
||||
|
||||
|
||||
async def test_get_progress_flow(hass, client, hass_admin_user):
|
||||
"""Test we can query the API for same result as we get from init a flow."""
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
async def async_step_user(self, user_input=None):
|
||||
schema = OrderedDict()
|
||||
schema[vol.Required('username')] = str
|
||||
schema[vol.Required('password')] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=schema,
|
||||
errors={
|
||||
'username': 'Should be unique.'
|
||||
}
|
||||
)
|
||||
|
||||
with patch.dict(HANDLERS, {'test': TestFlow}):
|
||||
resp = await client.post('/api/config/config_entries/flow',
|
||||
json={'handler': 'test'})
|
||||
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
|
||||
hass_admin_user.groups = []
|
||||
|
||||
resp2 = await client.get(
|
||||
'/api/config/config_entries/flow/{}'.format(data['flow_id']))
|
||||
|
||||
assert resp2.status == 401
|
||||
|
|
Loading…
Reference in New Issue