Skip to content

Commit

Permalink
Merge 85e6f92 into 9efb90d
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob committed Dec 13, 2018
2 parents 9efb90d + 85e6f92 commit e007da8
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
1 change: 1 addition & 0 deletions homeassistant/auth/permissions/const.py
@@ -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
@@ -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
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
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

0 comments on commit e007da8

Please sign in to comment.