Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions homeassistant/auth/permissions/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Permission constants."""
CAT_ENTITIES = 'entities'
CAT_CONFIG_ENTRIES = 'config_entries'
SUBCAT_ALL = 'all'

POLICY_READ = 'read'
Expand Down
37 changes: 37 additions & 0 deletions homeassistant/components/config/config_entries.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -85,19 +90,51 @@ async def get(self, request):
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."""

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."""
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
130 changes: 130 additions & 0 deletions tests/components/config/test_config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -155,6 +166,35 @@ def async_step_user(self, user_input=None):
}


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."""
Expand Down Expand Up @@ -273,6 +313,58 @@ def async_step_account(self, user_input=None):
}


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."""
Expand Down Expand Up @@ -305,6 +397,13 @@ def async_step_account(self, user_input=None):
]


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 = []
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."""
Expand Down Expand Up @@ -337,3 +436,34 @@ def async_step_user(self, user_input=None):
data2 = yield from resp2.json()

assert data == data2


async def test_get_progress_flow_unauth(hass, client, hass_admin_user):
"""Test we can can't query the API for result of 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