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
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:

View File

@ -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()

View File

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

View File

@ -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()

View File

@ -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"

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")