2018-05-10 18:09:22 +00:00
|
|
|
"""Test the Home Assistant local auth provider."""
|
2019-03-04 23:55:26 +00:00
|
|
|
import asyncio
|
2019-01-16 23:03:05 +00:00
|
|
|
from unittest.mock import Mock, patch
|
2018-07-13 13:31:20 +00:00
|
|
|
|
2018-05-10 18:09:22 +00:00
|
|
|
import pytest
|
2018-08-28 18:54:01 +00:00
|
|
|
import voluptuous as vol
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
from homeassistant import data_entry_flow
|
2018-08-21 18:03:38 +00:00
|
|
|
from homeassistant.auth import auth_manager_from_config, auth_store
|
2018-07-13 13:31:20 +00:00
|
|
|
from homeassistant.auth.providers import (
|
|
|
|
auth_provider_from_config, homeassistant as hass_auth)
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2019-01-16 23:03:05 +00:00
|
|
|
from tests.common import mock_coro
|
|
|
|
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def data(hass):
|
|
|
|
"""Create a loaded data class."""
|
|
|
|
data = hass_auth.Data(hass)
|
|
|
|
hass.loop.run_until_complete(data.async_load())
|
|
|
|
return data
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
|
2019-01-16 23:03:05 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def legacy_data(hass):
|
|
|
|
"""Create a loaded legacy data class."""
|
|
|
|
data = hass_auth.Data(hass)
|
|
|
|
hass.loop.run_until_complete(data.async_load())
|
|
|
|
data.is_legacy = True
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
async def test_validating_password_invalid_user(data, hass):
|
|
|
|
"""Test validating an invalid user."""
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
|
|
|
data.validate_login('non-existing', 'pw')
|
|
|
|
|
|
|
|
|
|
|
|
async def test_not_allow_set_id():
|
|
|
|
"""Test we are not allowed to set an ID in config."""
|
|
|
|
hass = Mock()
|
|
|
|
with pytest.raises(vol.Invalid):
|
|
|
|
await auth_provider_from_config(hass, None, {
|
|
|
|
'type': 'homeassistant',
|
|
|
|
'id': 'invalid',
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async def test_new_users_populate_values(hass, data):
|
|
|
|
"""Test that we populate data for new users."""
|
|
|
|
data.add_auth('hello', 'test-pass')
|
|
|
|
await data.async_save()
|
|
|
|
|
|
|
|
manager = await auth_manager_from_config(hass, [{
|
|
|
|
'type': 'homeassistant'
|
|
|
|
}], [])
|
|
|
|
provider = manager.auth_providers[0]
|
|
|
|
credentials = await provider.async_get_or_create_credentials({
|
|
|
|
'username': 'hello'
|
|
|
|
})
|
|
|
|
user = await manager.async_get_or_create_user(credentials)
|
|
|
|
assert user.name == 'hello'
|
|
|
|
assert user.is_active
|
|
|
|
|
|
|
|
|
|
|
|
async def test_changing_password_raises_invalid_user(data, hass):
|
|
|
|
"""Test that changing password raises invalid user."""
|
|
|
|
with pytest.raises(hass_auth.InvalidUser):
|
|
|
|
data.change_password('non-existing', 'pw')
|
|
|
|
|
|
|
|
|
|
|
|
# Modern mode
|
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
async def test_adding_user(data, hass):
|
2018-05-10 18:09:22 +00:00
|
|
|
"""Test adding a user."""
|
2018-07-13 13:31:20 +00:00
|
|
|
data.add_auth('test-user', 'test-pass')
|
2019-01-16 23:03:05 +00:00
|
|
|
data.validate_login(' test-user ', 'test-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
async def test_adding_user_duplicate_username(data, hass):
|
2018-08-21 18:03:38 +00:00
|
|
|
"""Test adding a user with duplicate username."""
|
2018-07-13 13:31:20 +00:00
|
|
|
data.add_auth('test-user', 'test-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
with pytest.raises(hass_auth.InvalidUser):
|
2019-01-29 07:28:52 +00:00
|
|
|
data.add_auth('TEST-user ', 'other-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
async def test_validating_password_invalid_password(data, hass):
|
2018-08-21 18:03:38 +00:00
|
|
|
"""Test validating an invalid password."""
|
2018-07-13 13:31:20 +00:00
|
|
|
data.add_auth('test-user', 'test-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
2019-01-16 23:03:05 +00:00
|
|
|
data.validate_login(' test-user ', 'invalid-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2019-01-29 07:28:52 +00:00
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
|
|
|
data.validate_login('test-user', 'test-pass ')
|
|
|
|
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
|
|
|
data.validate_login('test-user', 'Test-pass')
|
|
|
|
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
async def test_changing_password(data, hass):
|
2018-05-10 18:09:22 +00:00
|
|
|
"""Test adding a user."""
|
2019-01-16 23:03:05 +00:00
|
|
|
data.add_auth('test-user', 'test-pass')
|
2019-01-29 07:28:52 +00:00
|
|
|
data.change_password('TEST-USER ', 'new-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
2019-01-16 23:03:05 +00:00
|
|
|
data.validate_login('test-user', 'test-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2019-01-29 07:28:52 +00:00
|
|
|
data.validate_login('test-UsEr', 'new-pass')
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
async def test_login_flow_validates(data, hass):
|
2018-05-10 18:09:22 +00:00
|
|
|
"""Test login flow."""
|
2018-07-13 13:31:20 +00:00
|
|
|
data.add_auth('test-user', 'test-pass')
|
2018-06-29 04:02:45 +00:00
|
|
|
await data.async_save()
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2018-08-21 18:03:38 +00:00
|
|
|
provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass),
|
|
|
|
{'type': 'homeassistant'})
|
|
|
|
flow = await provider.async_login_flow({})
|
2018-05-10 18:09:22 +00:00
|
|
|
result = await flow.async_step_init()
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
result = await flow.async_step_init({
|
|
|
|
'username': 'incorrect-user',
|
|
|
|
'password': 'test-pass',
|
|
|
|
})
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
|
|
|
assert result['errors']['base'] == 'invalid_auth'
|
|
|
|
|
|
|
|
result = await flow.async_step_init({
|
2019-01-29 07:28:52 +00:00
|
|
|
'username': 'TEST-user ',
|
2018-06-29 04:02:45 +00:00
|
|
|
'password': 'incorrect-pass',
|
|
|
|
})
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
|
|
|
assert result['errors']['base'] == 'invalid_auth'
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
result = await flow.async_step_init({
|
2019-01-29 07:28:52 +00:00
|
|
|
'username': 'test-USER',
|
2018-06-29 04:02:45 +00:00
|
|
|
'password': 'test-pass',
|
|
|
|
})
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
2019-01-29 07:28:52 +00:00
|
|
|
assert result['data']['username'] == 'test-USER'
|
2018-05-10 18:09:22 +00:00
|
|
|
|
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
async def test_saving_loading(data, hass):
|
|
|
|
"""Test saving and loading JSON."""
|
2018-07-13 13:31:20 +00:00
|
|
|
data.add_auth('test-user', 'test-pass')
|
|
|
|
data.add_auth('second-user', 'second-pass')
|
2018-06-29 04:02:45 +00:00
|
|
|
await data.async_save()
|
2018-05-10 18:09:22 +00:00
|
|
|
|
2018-06-29 04:02:45 +00:00
|
|
|
data = hass_auth.Data(hass)
|
|
|
|
await data.async_load()
|
2019-01-16 23:03:05 +00:00
|
|
|
data.validate_login('test-user ', 'test-pass')
|
|
|
|
data.validate_login('second-user ', 'second-pass')
|
2018-07-13 13:31:20 +00:00
|
|
|
|
|
|
|
|
2019-01-16 23:03:05 +00:00
|
|
|
async def test_get_or_create_credentials(hass, data):
|
|
|
|
"""Test that we can get or create credentials."""
|
|
|
|
manager = await auth_manager_from_config(hass, [{
|
|
|
|
'type': 'homeassistant'
|
|
|
|
}], [])
|
|
|
|
provider = manager.auth_providers[0]
|
|
|
|
provider.data = data
|
|
|
|
credentials1 = await provider.async_get_or_create_credentials({
|
|
|
|
'username': 'hello'
|
|
|
|
})
|
|
|
|
with patch.object(provider, 'async_credentials',
|
|
|
|
return_value=mock_coro([credentials1])):
|
|
|
|
credentials2 = await provider.async_get_or_create_credentials({
|
|
|
|
'username': 'hello '
|
2018-08-28 18:54:01 +00:00
|
|
|
})
|
2019-01-16 23:03:05 +00:00
|
|
|
assert credentials1 is credentials2
|
2018-07-19 20:10:36 +00:00
|
|
|
|
|
|
|
|
2019-01-16 23:03:05 +00:00
|
|
|
# Legacy mode
|
|
|
|
|
|
|
|
async def test_legacy_adding_user(legacy_data, hass):
|
|
|
|
"""Test in legacy mode adding a user."""
|
|
|
|
legacy_data.add_auth('test-user', 'test-pass')
|
|
|
|
legacy_data.validate_login('test-user', 'test-pass')
|
|
|
|
|
|
|
|
|
|
|
|
async def test_legacy_adding_user_duplicate_username(legacy_data, hass):
|
|
|
|
"""Test in legacy mode adding a user with duplicate username."""
|
|
|
|
legacy_data.add_auth('test-user', 'test-pass')
|
|
|
|
with pytest.raises(hass_auth.InvalidUser):
|
|
|
|
legacy_data.add_auth('test-user', 'other-pass')
|
2019-01-29 07:28:52 +00:00
|
|
|
# Not considered duplicate
|
|
|
|
legacy_data.add_auth('test-user ', 'test-pass')
|
|
|
|
legacy_data.add_auth('Test-user', 'test-pass')
|
2019-01-16 23:03:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_legacy_validating_password_invalid_password(legacy_data, hass):
|
|
|
|
"""Test in legacy mode validating an invalid password."""
|
|
|
|
legacy_data.add_auth('test-user', 'test-pass')
|
|
|
|
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
|
|
|
legacy_data.validate_login('test-user', 'invalid-pass')
|
|
|
|
|
2018-07-19 20:10:36 +00:00
|
|
|
|
2019-01-16 23:03:05 +00:00
|
|
|
async def test_legacy_changing_password(legacy_data, hass):
|
|
|
|
"""Test in legacy mode adding a user."""
|
|
|
|
user = 'test-user'
|
|
|
|
legacy_data.add_auth(user, 'test-pass')
|
|
|
|
legacy_data.change_password(user, 'new-pass')
|
|
|
|
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
|
|
|
legacy_data.validate_login(user, 'test-pass')
|
|
|
|
|
|
|
|
legacy_data.validate_login(user, 'new-pass')
|
|
|
|
|
|
|
|
|
|
|
|
async def test_legacy_changing_password_raises_invalid_user(legacy_data, hass):
|
|
|
|
"""Test in legacy mode that we initialize an empty config."""
|
|
|
|
with pytest.raises(hass_auth.InvalidUser):
|
|
|
|
legacy_data.change_password('non-existing', 'pw')
|
|
|
|
|
|
|
|
|
|
|
|
async def test_legacy_login_flow_validates(legacy_data, hass):
|
|
|
|
"""Test in legacy mode login flow."""
|
|
|
|
legacy_data.add_auth('test-user', 'test-pass')
|
|
|
|
await legacy_data.async_save()
|
|
|
|
|
|
|
|
provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass),
|
|
|
|
{'type': 'homeassistant'})
|
|
|
|
flow = await provider.async_login_flow({})
|
|
|
|
result = await flow.async_step_init()
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
|
|
|
|
|
|
|
result = await flow.async_step_init({
|
|
|
|
'username': 'incorrect-user',
|
|
|
|
'password': 'test-pass',
|
|
|
|
})
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
|
|
|
assert result['errors']['base'] == 'invalid_auth'
|
|
|
|
|
|
|
|
result = await flow.async_step_init({
|
|
|
|
'username': 'test-user',
|
|
|
|
'password': 'incorrect-pass',
|
|
|
|
})
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
|
|
|
assert result['errors']['base'] == 'invalid_auth'
|
|
|
|
|
|
|
|
result = await flow.async_step_init({
|
|
|
|
'username': 'test-user',
|
|
|
|
'password': 'test-pass',
|
|
|
|
})
|
|
|
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
|
|
|
assert result['data']['username'] == 'test-user'
|
|
|
|
|
|
|
|
|
|
|
|
async def test_legacy_saving_loading(legacy_data, hass):
|
|
|
|
"""Test in legacy mode saving and loading JSON."""
|
|
|
|
legacy_data.add_auth('test-user', 'test-pass')
|
|
|
|
legacy_data.add_auth('second-user', 'second-pass')
|
|
|
|
await legacy_data.async_save()
|
|
|
|
|
|
|
|
legacy_data = hass_auth.Data(hass)
|
|
|
|
await legacy_data.async_load()
|
|
|
|
legacy_data.is_legacy = True
|
|
|
|
legacy_data.validate_login('test-user', 'test-pass')
|
|
|
|
legacy_data.validate_login('second-user', 'second-pass')
|
|
|
|
|
|
|
|
with pytest.raises(hass_auth.InvalidAuth):
|
|
|
|
legacy_data.validate_login('test-user ', 'test-pass')
|
|
|
|
|
|
|
|
|
|
|
|
async def test_legacy_get_or_create_credentials(hass, legacy_data):
|
|
|
|
"""Test in legacy mode that we can get or create credentials."""
|
2018-07-19 20:10:36 +00:00
|
|
|
manager = await auth_manager_from_config(hass, [{
|
|
|
|
'type': 'homeassistant'
|
2018-08-22 07:52:34 +00:00
|
|
|
}], [])
|
2018-07-19 20:10:36 +00:00
|
|
|
provider = manager.auth_providers[0]
|
2019-01-16 23:03:05 +00:00
|
|
|
provider.data = legacy_data
|
|
|
|
credentials1 = await provider.async_get_or_create_credentials({
|
2018-07-19 20:10:36 +00:00
|
|
|
'username': 'hello'
|
|
|
|
})
|
2019-01-16 23:03:05 +00:00
|
|
|
|
|
|
|
with patch.object(provider, 'async_credentials',
|
|
|
|
return_value=mock_coro([credentials1])):
|
|
|
|
credentials2 = await provider.async_get_or_create_credentials({
|
|
|
|
'username': 'hello'
|
|
|
|
})
|
|
|
|
assert credentials1 is credentials2
|
|
|
|
|
|
|
|
with patch.object(provider, 'async_credentials',
|
|
|
|
return_value=mock_coro([credentials1])):
|
|
|
|
credentials3 = await provider.async_get_or_create_credentials({
|
|
|
|
'username': 'hello '
|
|
|
|
})
|
|
|
|
assert credentials1 is not credentials3
|
2019-03-04 23:55:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def test_race_condition_in_data_loading(hass):
|
|
|
|
"""Test race condition in the hass_auth.Data loading.
|
|
|
|
|
|
|
|
Ref issue: https://github.com/home-assistant/home-assistant/issues/21569
|
|
|
|
"""
|
|
|
|
counter = 0
|
|
|
|
|
|
|
|
async def mock_load(_):
|
|
|
|
"""Mock of homeassistant.helpers.storage.Store.async_load."""
|
|
|
|
nonlocal counter
|
|
|
|
counter += 1
|
|
|
|
await asyncio.sleep(0)
|
|
|
|
|
|
|
|
provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass),
|
|
|
|
{'type': 'homeassistant'})
|
|
|
|
with patch('homeassistant.helpers.storage.Store.async_load',
|
|
|
|
new=mock_load):
|
|
|
|
task1 = provider.async_validate_login('user', 'pass')
|
|
|
|
task2 = provider.async_validate_login('user', 'pass')
|
|
|
|
results = await asyncio.gather(task1, task2, return_exceptions=True)
|
|
|
|
assert counter == 1
|
|
|
|
assert isinstance(results[0], hass_auth.InvalidAuth)
|
|
|
|
# results[1] will be a TypeError if race condition occurred
|
|
|
|
assert isinstance(results[1], hass_auth.InvalidAuth)
|