Speedup HA core auth (#2144)

* Speedup HA core auth

* Add reset API call

* use delete

* Add complexe cache logic

* Allow manage api to handle auth reset/cache

* revert to only cache

* add tests

* ignore protected-access for this tests

* fix comment
This commit is contained in:
Pascal Vizeli 2020-10-19 16:38:28 +02:00 committed by GitHub
parent 7a1d85ca2b
commit 9c53caae80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 131 additions and 5 deletions

View File

@ -221,7 +221,11 @@ class RestAPI(CoreSysAttributes):
api_auth.coresys = self.coresys api_auth.coresys = self.coresys
self.webapp.add_routes( self.webapp.add_routes(
[web.post("/auth", api_auth.auth), web.post("/auth/reset", api_auth.reset)] [
web.post("/auth", api_auth.auth),
web.post("/auth/reset", api_auth.reset),
web.delete("/auth/cache", api_auth.cache),
]
) )
def _register_supervisor(self) -> None: def _register_supervisor(self) -> None:

View File

@ -86,3 +86,8 @@ class APIAuth(CoreSysAttributes):
await asyncio.shield( await asyncio.shield(
self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD]) self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD])
) )
@api_process
async def cache(self, request: web.Request) -> None:
"""Process cache reset request."""
self.sys_auth.reset_data()

View File

@ -82,6 +82,7 @@ ADDONS_ROLE_ACCESS = {
r"^(?:" r"^(?:"
r"|/addons(?:/[^/]+/(?!security).+|/reload)?" r"|/addons(?:/[^/]+/(?!security).+|/reload)?"
r"|/audio/.+" r"|/audio/.+"
r"|/auth/cache"
r"|/cli/.+" r"|/cli/.+"
r"|/core/.+" r"|/core/.+"
r"|/dns/.+" r"|/dns/.+"

View File

@ -1,6 +1,8 @@
"""Manage SSO for Add-ons with Home Assistant user.""" """Manage SSO for Add-ons with Home Assistant user."""
import asyncio
import hashlib import hashlib
import logging import logging
from typing import Dict, Optional
from .addons.addon import Addon from .addons.addon import Addon
from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME, FILE_HASSIO_AUTH from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME, FILE_HASSIO_AUTH
@ -20,16 +22,21 @@ class Auth(JsonConfig, CoreSysAttributes):
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG) super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
def _check_cache(self, username: str, password: str) -> bool: self._running: Dict[str, asyncio.Task] = {}
def _check_cache(self, username: str, password: str) -> Optional[bool]:
"""Check password in cache.""" """Check password in cache."""
username_h = self._rehash(username) username_h = self._rehash(username)
password_h = self._rehash(password, username) password_h = self._rehash(password, username)
if username_h not in self._data:
_LOGGER.debug("Username '%s' not is in cache", username)
return None
# check cache
if self._data.get(username_h) == password_h: if self._data.get(username_h) == password_h:
_LOGGER.debug("Username '%s' is in cache", username) _LOGGER.debug("Username '%s' is in cache", username)
return True return True
_LOGGER.warning("Username '%s' not is in cache", username)
return False return False
def _update_cache(self, username: str, password: str) -> None: def _update_cache(self, username: str, password: str) -> None:
@ -61,11 +68,29 @@ class Auth(JsonConfig, CoreSysAttributes):
raise AuthError() raise AuthError()
_LOGGER.info("Auth request from '%s' for '%s'", addon.slug, username) _LOGGER.info("Auth request from '%s' for '%s'", addon.slug, username)
# Get from cache
cache_hit = self._check_cache(username, password)
# Check API state # Check API state
if not await self.sys_homeassistant.api.check_api_state(): if not await self.sys_homeassistant.api.check_api_state():
_LOGGER.debug("Home Assistant not running, checking cache") _LOGGER.debug("Home Assistant not running, checking cache")
return self._check_cache(username, password) return cache_hit is True
# No cache hit
if cache_hit is None:
return await self._backend_login(addon, username, password)
# Home Assistant Core take over 1-2sec to validate it
# Let's use the cache and update the cache in background
if username not in self._running:
self._running[username] = self.sys_create_task(
self._backend_login(addon, username, password)
)
return cache_hit
async def _backend_login(self, addon: Addon, username: str, password: str) -> bool:
"""Check username login on core."""
try: try:
async with self.sys_homeassistant.api.make_request( async with self.sys_homeassistant.api.make_request(
"post", "post",
@ -87,6 +112,8 @@ class Auth(JsonConfig, CoreSysAttributes):
return False return False
except HomeAssistantAPIError: except HomeAssistantAPIError:
_LOGGER.error("Can't request auth on Home Assistant!") _LOGGER.error("Can't request auth on Home Assistant!")
finally:
self._running.pop(username, None)
raise AuthError() raise AuthError()

View File

@ -107,6 +107,9 @@ async def coresys(loop, docker, dbus, network_manager, aiohttp_client) -> CoreSy
# Mock save json # Mock save json
coresys_obj.ingress.save_data = MagicMock() coresys_obj.ingress.save_data = MagicMock()
coresys_obj.auth.save_data = MagicMock()
coresys_obj.updater.save_data = MagicMock()
coresys_obj.config.save_data = MagicMock()
# Mock test client # Mock test client
coresys_obj.arch._default_arch = "amd64" coresys_obj.arch._default_arch = "amd64"

86
tests/test_auth.py Normal file
View File

@ -0,0 +1,86 @@
"""Test auth object."""
import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest
# pylint: disable=protected-access
@pytest.fixture(name="mock_auth_backend", autouse=True)
def mock_auth_backend_fixture(coresys):
"""Fix auth backend request."""
mock_auth_backend = AsyncMock()
coresys.auth._backend_login = mock_auth_backend
yield mock_auth_backend
@pytest.fixture(name="mock_api_state", autouse=True)
def mock_api_state_fixture(coresys):
"""Fix auth backend request."""
mock_api_state = AsyncMock()
coresys.homeassistant.api.check_api_state = mock_api_state
yield mock_api_state
@pytest.mark.asyncio
async def test_auth_request_with_backend(coresys, mock_auth_backend, mock_api_state):
"""Make simple auth request."""
addon = MagicMock()
mock_auth_backend.return_value = True
mock_api_state.return_value = True
assert await coresys.auth.check_login(addon, "username", "password")
assert mock_auth_backend.called
@pytest.mark.asyncio
async def test_auth_request_without_backend(coresys, mock_auth_backend, mock_api_state):
"""Make simple auth without request."""
addon = MagicMock()
mock_auth_backend.return_value = True
mock_api_state.return_value = False
assert not await coresys.auth.check_login(addon, "username", "password")
assert not mock_auth_backend.called
@pytest.mark.asyncio
async def test_auth_request_without_backend_cache(
coresys, mock_auth_backend, mock_api_state
):
"""Make simple auth without request."""
addon = MagicMock()
mock_auth_backend.return_value = True
mock_api_state.return_value = False
coresys.auth._update_cache("username", "password")
assert await coresys.auth.check_login(addon, "username", "password")
assert not mock_auth_backend.called
@pytest.mark.asyncio
async def test_auth_request_with_backend_cache_update(
coresys, mock_auth_backend, mock_api_state
):
"""Make simple auth without request and cache update."""
addon = MagicMock()
mock_auth_backend.return_value = False
mock_api_state.return_value = True
coresys.auth._update_cache("username", "password")
assert await coresys.auth.check_login(addon, "username", "password")
await asyncio.sleep(0)
assert mock_auth_backend.called
coresys.auth._dismatch_cache("username", "password")
assert not await coresys.auth.check_login(addon, "username", "password")