Allow usernames to be case-insensitive (#20558)

* Allow usernames to be case-insensitive

* Fix typing

* FLAKE*
This commit is contained in:
Paulus Schoutsen 2019-01-28 23:28:52 -08:00 committed by Pascal Vizeli
parent b0ff51b0ef
commit 73a0c664b8
2 changed files with 40 additions and 11 deletions

View File

@ -2,7 +2,8 @@
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
import logging import logging
from typing import Any, Dict, List, Optional, cast
from typing import Any, Dict, List, Optional, Set, cast # noqa: F401
import bcrypt import bcrypt
import voluptuous as vol import voluptuous as vol
@ -52,6 +53,9 @@ class Data:
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True) private=True)
self._data = None # type: Optional[Dict[str, Any]] self._data = None # type: Optional[Dict[str, Any]]
# Legacy mode will allow usernames to start/end with whitespace
# and will compare usernames case-insensitive.
# Remove in 2020 or when we launch 1.0.
self.is_legacy = False self.is_legacy = False
@callback @callback
@ -60,7 +64,7 @@ class Data:
if self.is_legacy: if self.is_legacy:
return username return username
return username.strip() return username.strip().casefold()
async def async_load(self) -> None: async def async_load(self) -> None:
"""Load stored data.""" """Load stored data."""
@ -71,9 +75,26 @@ class Data:
'users': [] 'users': []
} }
seen = set() # type: Set[str]
for user in data['users']: for user in data['users']:
username = user['username'] username = user['username']
# check if we have duplicates
folded = username.casefold()
if folded in seen:
self.is_legacy = True
logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode "
"because we detected usernames that are case-insensitive"
"equivalent. Please change the username: '%s'.", username)
break
seen.add(folded)
# check if we have unstripped usernames # check if we have unstripped usernames
if username != username.strip(): if username != username.strip():
self.is_legacy = True self.is_legacy = True
@ -81,7 +102,7 @@ class Data:
logging.getLogger(__name__).warning( logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode " "Home Assistant auth provider is running in legacy mode "
"because we detected usernames that start or end in a " "because we detected usernames that start or end in a "
"space. Please change the username.") "space. Please change the username: '%s'.", username)
break break
@ -103,7 +124,7 @@ class Data:
# Compare all users to avoid timing attacks. # Compare all users to avoid timing attacks.
for user in self.users: for user in self.users:
if username == user['username']: if self.normalize_username(user['username']) == username:
found = user found = user
if found is None: if found is None:

View File

@ -73,7 +73,6 @@ async def test_changing_password_raises_invalid_user(data, hass):
async def test_adding_user(data, hass): async def test_adding_user(data, hass):
"""Test adding a user.""" """Test adding a user."""
data.add_auth('test-user', 'test-pass') data.add_auth('test-user', 'test-pass')
data.validate_login('test-user', 'test-pass')
data.validate_login(' test-user ', 'test-pass') data.validate_login(' test-user ', 'test-pass')
@ -81,7 +80,7 @@ async def test_adding_user_duplicate_username(data, hass):
"""Test adding a user with duplicate username.""" """Test adding a user with duplicate username."""
data.add_auth('test-user', 'test-pass') data.add_auth('test-user', 'test-pass')
with pytest.raises(hass_auth.InvalidUser): with pytest.raises(hass_auth.InvalidUser):
data.add_auth('test-user ', 'other-pass') data.add_auth('TEST-user ', 'other-pass')
async def test_validating_password_invalid_password(data, hass): async def test_validating_password_invalid_password(data, hass):
@ -91,16 +90,22 @@ async def test_validating_password_invalid_password(data, hass):
with pytest.raises(hass_auth.InvalidAuth): with pytest.raises(hass_auth.InvalidAuth):
data.validate_login(' test-user ', 'invalid-pass') data.validate_login(' test-user ', 'invalid-pass')
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')
async def test_changing_password(data, hass): async def test_changing_password(data, hass):
"""Test adding a user.""" """Test adding a user."""
data.add_auth('test-user', 'test-pass') data.add_auth('test-user', 'test-pass')
data.change_password('test-user ', 'new-pass') data.change_password('TEST-USER ', 'new-pass')
with pytest.raises(hass_auth.InvalidAuth): with pytest.raises(hass_auth.InvalidAuth):
data.validate_login('test-user', 'test-pass') data.validate_login('test-user', 'test-pass')
data.validate_login('test-user', 'new-pass') data.validate_login('test-UsEr', 'new-pass')
async def test_login_flow_validates(data, hass): async def test_login_flow_validates(data, hass):
@ -122,18 +127,18 @@ async def test_login_flow_validates(data, hass):
assert result['errors']['base'] == 'invalid_auth' assert result['errors']['base'] == 'invalid_auth'
result = await flow.async_step_init({ result = await flow.async_step_init({
'username': 'test-user ', 'username': 'TEST-user ',
'password': 'incorrect-pass', 'password': 'incorrect-pass',
}) })
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['errors']['base'] == 'invalid_auth' assert result['errors']['base'] == 'invalid_auth'
result = await flow.async_step_init({ result = await flow.async_step_init({
'username': 'test-user', 'username': 'test-USER',
'password': 'test-pass', 'password': 'test-pass',
}) })
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data']['username'] == 'test-user' assert result['data']['username'] == 'test-USER'
async def test_saving_loading(data, hass): async def test_saving_loading(data, hass):
@ -179,6 +184,9 @@ async def test_legacy_adding_user_duplicate_username(legacy_data, hass):
legacy_data.add_auth('test-user', 'test-pass') legacy_data.add_auth('test-user', 'test-pass')
with pytest.raises(hass_auth.InvalidUser): with pytest.raises(hass_auth.InvalidUser):
legacy_data.add_auth('test-user', 'other-pass') legacy_data.add_auth('test-user', 'other-pass')
# Not considered duplicate
legacy_data.add_auth('test-user ', 'test-pass')
legacy_data.add_auth('Test-user', 'test-pass')
async def test_legacy_validating_password_invalid_password(legacy_data, hass): async def test_legacy_validating_password_invalid_password(legacy_data, hass):