mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Allow usernames to be case-insensitive (#20558)
* Allow usernames to be case-insensitive * Fix typing * FLAKE*
This commit is contained in:
parent
b0ff51b0ef
commit
73a0c664b8
@ -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:
|
||||||
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user