Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Legacy api fix #18733

Merged
merged 3 commits into from Nov 27, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 25 additions & 4 deletions homeassistant/auth/providers/legacy_api_password.py
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
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
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
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
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 @@ -88,16 +89,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
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
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
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
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
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
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