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:
parent
7a1d85ca2b
commit
9c53caae80
|
@ -221,7 +221,11 @@ class RestAPI(CoreSysAttributes):
|
|||
api_auth.coresys = self.coresys
|
||||
|
||||
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:
|
||||
|
|
|
@ -86,3 +86,8 @@ class APIAuth(CoreSysAttributes):
|
|||
await asyncio.shield(
|
||||
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()
|
||||
|
|
|
@ -82,6 +82,7 @@ ADDONS_ROLE_ACCESS = {
|
|||
r"^(?:"
|
||||
r"|/addons(?:/[^/]+/(?!security).+|/reload)?"
|
||||
r"|/audio/.+"
|
||||
r"|/auth/cache"
|
||||
r"|/cli/.+"
|
||||
r"|/core/.+"
|
||||
r"|/dns/.+"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
from .addons.addon import Addon
|
||||
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)
|
||||
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."""
|
||||
username_h = self._rehash(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:
|
||||
_LOGGER.debug("Username '%s' is in cache", username)
|
||||
return True
|
||||
|
||||
_LOGGER.warning("Username '%s' not is in cache", username)
|
||||
return False
|
||||
|
||||
def _update_cache(self, username: str, password: str) -> None:
|
||||
|
@ -61,11 +68,29 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||
raise AuthError()
|
||||
_LOGGER.info("Auth request from '%s' for '%s'", addon.slug, username)
|
||||
|
||||
# Get from cache
|
||||
cache_hit = self._check_cache(username, password)
|
||||
|
||||
# Check API state
|
||||
if not await self.sys_homeassistant.api.check_api_state():
|
||||
_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:
|
||||
async with self.sys_homeassistant.api.make_request(
|
||||
"post",
|
||||
|
@ -87,6 +112,8 @@ class Auth(JsonConfig, CoreSysAttributes):
|
|||
return False
|
||||
except HomeAssistantAPIError:
|
||||
_LOGGER.error("Can't request auth on Home Assistant!")
|
||||
finally:
|
||||
self._running.pop(username, None)
|
||||
|
||||
raise AuthError()
|
||||
|
||||
|
|
|
@ -107,6 +107,9 @@ async def coresys(loop, docker, dbus, network_manager, aiohttp_client) -> CoreSy
|
|||
|
||||
# Mock save json
|
||||
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
|
||||
coresys_obj.arch._default_arch = "amd64"
|
||||
|
|
|
@ -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")
|
Loading…
Reference in New Issue