Skip to content

Commit

Permalink
Legacy api fix (#18733)
Browse files Browse the repository at this point in the history
* Set user for API password requests

* Fix tests

* Fix typing
  • Loading branch information
balloob committed Nov 29, 2018
1 parent 6013893 commit ff33d34
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 74 deletions.
29 changes: 25 additions & 4 deletions homeassistant/auth/providers/legacy_api_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
It will be removed when auth system production ready
"""
import hmac
from typing import Any, Dict, Optional, cast
from typing import Any, Dict, Optional, cast, TYPE_CHECKING

import voluptuous as vol

from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError

from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
from .. import AuthManager
from ..models import Credentials, UserMeta, User

if TYPE_CHECKING:
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401


USER_SCHEMA = vol.Schema({
Expand All @@ -31,6 +34,24 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""


async def async_get_user(hass: HomeAssistant) -> User:
"""Return the legacy API password user."""
auth = cast(AuthManager, hass.auth) # type: ignore
found = None

for prv in auth.auth_providers:
if prv.type == 'legacy_api_password':
found = prv
break

if found is None:
raise ValueError('Legacy API password provider not found')

return await auth.async_get_or_create_user(
await found.async_get_or_create_credentials({})
)


@AUTH_PROVIDERS.register('legacy_api_password')
class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/http/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from homeassistant.core import callback
from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.auth.providers import legacy_api_password
from homeassistant.auth.util import generate_secret
from homeassistant.util import dt as dt_util

Expand Down Expand Up @@ -78,12 +79,16 @@ async def auth_middleware(request, handler):
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
# A valid auth header has been set
authenticated = True
request['hass_user'] = await legacy_api_password.async_get_user(
app['hass'])

elif (legacy_auth and DATA_API_PASSWORD in request.query and
hmac.compare_digest(
api_password.encode('utf-8'),
request.query[DATA_API_PASSWORD].encode('utf-8'))):
authenticated = True
request['hass_user'] = await legacy_api_password.async_get_user(
app['hass'])

elif _is_trusted_ip(request, trusted_networks):
authenticated = True
Expand Down
4 changes: 2 additions & 2 deletions tests/components/alexa/test_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@


@pytest.fixture
def alexa_client(loop, hass, aiohttp_client):
def alexa_client(loop, hass, hass_client):
"""Initialize a Home Assistant server for testing this module."""
@callback
def mock_service(call):
Expand Down Expand Up @@ -95,7 +95,7 @@ def mock_service(call):
},
}
}))
return loop.run_until_complete(aiohttp_client(hass.http.app))
return loop.run_until_complete(hass_client())


def _intent_req(client, data=None):
Expand Down
12 changes: 6 additions & 6 deletions tests/components/alexa/test_smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,10 +1437,10 @@ async def test_unsupported_domain(hass):
assert not msg['payload']['endpoints']


async def do_http_discovery(config, hass, aiohttp_client):
async def do_http_discovery(config, hass, hass_client):
"""Submit a request to the Smart Home HTTP API."""
await async_setup_component(hass, alexa.DOMAIN, config)
http_client = await aiohttp_client(hass.http.app)
http_client = await hass_client()

request = get_new_request('Alexa.Discovery', 'Discover')
response = await http_client.post(
Expand All @@ -1450,28 +1450,28 @@ async def do_http_discovery(config, hass, aiohttp_client):
return response


async def test_http_api(hass, aiohttp_client):
async def test_http_api(hass, hass_client):
"""With `smart_home:` HTTP API is exposed."""
config = {
'alexa': {
'smart_home': None
}
}

response = await do_http_discovery(config, hass, aiohttp_client)
response = await do_http_discovery(config, hass, hass_client)
response_data = await response.json()

# Here we're testing just the HTTP view glue -- details of discovery are
# covered in other tests.
assert response_data['event']['header']['name'] == 'Discover.Response'


async def test_http_api_disabled(hass, aiohttp_client):
async def test_http_api_disabled(hass, hass_client):
"""Without `smart_home:`, the HTTP API is disabled."""
config = {
'alexa': {}
}
response = await do_http_discovery(config, hass, aiohttp_client)
response = await do_http_discovery(config, hass, hass_client)

assert response.status == 404

Expand Down
39 changes: 37 additions & 2 deletions tests/components/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from homeassistant.auth.providers import legacy_api_password, homeassistant
from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.http import URL
from homeassistant.components.websocket_api.auth import (
Expand Down Expand Up @@ -80,16 +81,50 @@ def hass_access_token(hass, hass_admin_user):


@pytest.fixture
def hass_admin_user(hass):
def hass_admin_user(hass, local_auth):
"""Return a Home Assistant admin user."""
admin_group = hass.loop.run_until_complete(hass.auth.async_get_group(
GROUP_ID_ADMIN))
return MockUser(groups=[admin_group]).add_to_hass(hass)


@pytest.fixture
def hass_read_only_user(hass):
def hass_read_only_user(hass, local_auth):
"""Return a Home Assistant read only user."""
read_only_group = hass.loop.run_until_complete(hass.auth.async_get_group(
GROUP_ID_READ_ONLY))
return MockUser(groups=[read_only_group]).add_to_hass(hass)


@pytest.fixture
def legacy_auth(hass):
"""Load legacy API password provider."""
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
hass, hass.auth._store, {
'type': 'legacy_api_password'
}
)
hass.auth._providers[(prv.type, prv.id)] = prv


@pytest.fixture
def local_auth(hass):
"""Load local auth provider."""
prv = homeassistant.HassAuthProvider(
hass, hass.auth._store, {
'type': 'homeassistant'
}
)
hass.auth._providers[(prv.type, prv.id)] = prv


@pytest.fixture
def hass_client(hass, aiohttp_client, hass_access_token):
"""Return an authenticated HTTP client."""
async def auth_client():
"""Return an authenticated client."""
return await aiohttp_client(hass.http.app, headers={
'Authorization': "Bearer {}".format(hass_access_token)
})

return auth_client
2 changes: 1 addition & 1 deletion tests/components/hassio/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def hassio_env():


@pytest.fixture
def hassio_client(hassio_env, hass, aiohttp_client):
def hassio_client(hassio_env, hass, aiohttp_client, legacy_auth):
"""Create mock hassio http client."""
with patch('homeassistant.components.hassio.HassIO.update_hass_api',
Mock(return_value=mock_coro({"result": "ok"}))), \
Expand Down
11 changes: 7 additions & 4 deletions tests/components/http/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ async def test_access_without_password(app, aiohttp_client):
assert resp.status == 200


async def test_access_with_password_in_header(app, aiohttp_client):
async def test_access_with_password_in_header(app, aiohttp_client,
legacy_auth):
"""Test access with password in header."""
setup_auth(app, [], False, api_password=API_PASSWORD)
client = await aiohttp_client(app)
Expand All @@ -97,7 +98,7 @@ async def test_access_with_password_in_header(app, aiohttp_client):
assert req.status == 401


async def test_access_with_password_in_query(app, aiohttp_client):
async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth):
"""Test access with password in URL."""
setup_auth(app, [], False, api_password=API_PASSWORD)
client = await aiohttp_client(app)
Expand Down Expand Up @@ -219,7 +220,8 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client):
"{} should be trusted".format(remote_addr)


async def test_auth_active_blocked_api_password_access(app, aiohttp_client):
async def test_auth_active_blocked_api_password_access(
app, aiohttp_client, legacy_auth):
"""Test access using api_password should be blocked when auth.active."""
setup_auth(app, [], True, api_password=API_PASSWORD)
client = await aiohttp_client(app)
Expand All @@ -239,7 +241,8 @@ async def test_auth_active_blocked_api_password_access(app, aiohttp_client):
assert req.status == 401


async def test_auth_legacy_support_api_password_access(app, aiohttp_client):
async def test_auth_legacy_support_api_password_access(
app, aiohttp_client, legacy_auth):
"""Test access using api_password if auth.support_legacy."""
setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD)
client = await aiohttp_client(app)
Expand Down
2 changes: 1 addition & 1 deletion tests/components/http/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ async def test_api_no_base_url(hass):
assert hass.config.api.base_url == 'http://127.0.0.1:8123'


async def test_not_log_password(hass, aiohttp_client, caplog):
async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
"""Test access with password doesn't get logged."""
assert await async_setup_component(hass, 'api', {
'http': {
Expand Down
22 changes: 16 additions & 6 deletions tests/components/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@


@pytest.fixture
def mock_api_client(hass, aiohttp_client, hass_access_token):
def mock_api_client(hass, hass_client):
"""Start the Hass HTTP component and return admin API client."""
hass.loop.run_until_complete(async_setup_component(hass, 'api', {}))
return hass.loop.run_until_complete(aiohttp_client(hass.http.app, headers={
'Authorization': 'Bearer {}'.format(hass_access_token)
}))
return hass.loop.run_until_complete(hass_client())


@asyncio.coroutine
Expand Down Expand Up @@ -408,7 +406,7 @@ def _listen_count(hass):


async def test_api_error_log(hass, aiohttp_client, hass_access_token,
hass_admin_user):
hass_admin_user, legacy_auth):
"""Test if we can fetch the error log."""
hass.data[DATA_LOGGING] = '/some/path'
await async_setup_component(hass, 'api', {
Expand Down Expand Up @@ -566,5 +564,17 @@ async def test_rendering_template_admin(hass, mock_api_client,
hass_admin_user):
"""Test rendering a template requires admin."""
hass_admin_user.groups = []
resp = await mock_api_client.post('/api/template')
resp = await mock_api_client.post(const.URL_API_TEMPLATE)
assert resp.status == 401


async def test_rendering_template_legacy_user(
hass, mock_api_client, aiohttp_client, legacy_auth):
"""Test rendering a template with legacy API password."""
hass.states.async_set('sensor.temperature', 10)
client = await aiohttp_client(hass.http.app)
resp = await client.post(
const.URL_API_TEMPLATE,
json={"template": '{{ states.sensor.temperature.state }}'}
)
assert resp.status == 401
12 changes: 6 additions & 6 deletions tests/components/test_conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async def test_register_before_setup(hass):
assert intent.text_input == 'I would like the Grolsch beer'


async def test_http_processing_intent(hass, aiohttp_client):
async def test_http_processing_intent(hass, hass_client):
"""Test processing intent via HTTP API."""
class TestIntentHandler(intent.IntentHandler):
"""Test Intent Handler."""
Expand Down Expand Up @@ -120,7 +120,7 @@ async def async_handle(self, intent):
})
assert result

client = await aiohttp_client(hass.http.app)
client = await hass_client()
resp = await client.post('/api/conversation/process', json={
'text': 'I would like the Grolsch beer'
})
Expand Down Expand Up @@ -244,15 +244,15 @@ async def test_toggle_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}


async def test_http_api(hass, aiohttp_client):
async def test_http_api(hass, hass_client):
"""Test the HTTP conversation API."""
result = await component.async_setup(hass, {})
assert result

result = await async_setup_component(hass, 'conversation', {})
assert result

client = await aiohttp_client(hass.http.app)
client = await hass_client()
hass.states.async_set('light.kitchen', 'off')
calls = async_mock_service(hass, HASS_DOMAIN, 'turn_on')

Expand All @@ -268,15 +268,15 @@ async def test_http_api(hass, aiohttp_client):
assert call.data == {'entity_id': 'light.kitchen'}


async def test_http_api_wrong_data(hass, aiohttp_client):
async def test_http_api_wrong_data(hass, hass_client):
"""Test the HTTP conversation API."""
result = await component.async_setup(hass, {})
assert result

result = await async_setup_component(hass, 'conversation', {})
assert result

client = await aiohttp_client(hass.http.app)
client = await hass_client()

resp = await client.post('/api/conversation/process', json={
'text': 123
Expand Down
4 changes: 2 additions & 2 deletions tests/components/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,13 @@ def set_state(entity_id, state, **kwargs):
return zero, four, states


async def test_fetch_period_api(hass, aiohttp_client):
async def test_fetch_period_api(hass, hass_client):
"""Test the fetch period view for history."""
await hass.async_add_job(init_recorder_component, hass)
await async_setup_component(hass, 'history', {})
await hass.components.recorder.wait_connection_ready()
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await aiohttp_client(hass.http.app)
client = await hass_client()
response = await client.get(
'/api/history/period/{}'.format(dt_util.utcnow().isoformat()))
assert response.status == 200
Loading

0 comments on commit ff33d34

Please sign in to comment.