mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 21:56:29 +00:00
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
|
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:
|
||||||
|
@ -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()
|
||||||
|
@ -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/.+"
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
86
tests/test_auth.py
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user