mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 15:16:33 +00:00
Add-on SSO support with Home Assistant auth system (#752)
* Create auth.py * Finish auth cache * Add documentation * Add valid schema * Update auth.py * Update auth.py * Update security.py * Create auth.py * Update coresys.py * Update bootstrap.py * Update const.py * Update validate.py * Update const.py * Update addon.py * Update auth.py * Update __init__.py * Update auth.py * Update auth.py * Update auth.py * Update const.py * Update auth.py * Update auth.py * Update auth.py * Update validate.py * Update coresys.py * Update auth.py * Update auth.py * more security * Update API.md * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update auth.py * Update homeassistant.py * Update homeassistant.py
This commit is contained in:
parent
7dbbcf24c8
commit
8443da0b9f
12
API.md
12
API.md
@ -675,3 +675,15 @@ return:
|
|||||||
"channel": "stable|beta|dev"
|
"channel": "stable|beta|dev"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Auth / SSO API
|
||||||
|
|
||||||
|
You can use the user system on homeassistant. We handle this auth system on
|
||||||
|
supervisor.
|
||||||
|
|
||||||
|
You can call post `/auth`
|
||||||
|
|
||||||
|
We support:
|
||||||
|
- Json `{ "user|name": "...", "password": "..." }`
|
||||||
|
- application/x-www-form-urlencoded `user|name=...&password=...`
|
||||||
|
- BasicAuth
|
||||||
|
@ -28,7 +28,7 @@ from ..const import (
|
|||||||
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
|
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
|
||||||
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
|
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
|
||||||
ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
|
ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE, ATTR_LOGIN_BACKEND,
|
||||||
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
|
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.addon import DockerAddon
|
from ..docker.addon import DockerAddon
|
||||||
@ -411,6 +411,11 @@ class Addon(CoreSysAttributes):
|
|||||||
"""Return True if the add-on read access to devicetree."""
|
"""Return True if the add-on read access to devicetree."""
|
||||||
return self._mesh[ATTR_DEVICETREE]
|
return self._mesh[ATTR_DEVICETREE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_login_backend(self):
|
||||||
|
"""Return True if the add-on access to login/auth backend."""
|
||||||
|
return self._mesh[ATTR_LOGIN_BACKEND]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_audio(self):
|
def with_audio(self):
|
||||||
"""Return True if the add-on access to audio."""
|
"""Return True if the add-on access to audio."""
|
||||||
|
@ -20,12 +20,13 @@ from ..const import (
|
|||||||
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
|
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
|
||||||
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
|
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
|
||||||
ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
|
ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE, ATTR_LOGIN_BACKEND,
|
||||||
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
|
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
|
||||||
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
|
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
|
||||||
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, PRIVILEGED_DAC_READ_SEARCH,
|
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, PRIVILEGED_DAC_READ_SEARCH,
|
||||||
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN)
|
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN)
|
||||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH
|
from ..validate import (
|
||||||
|
NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH, SHA256)
|
||||||
from ..services.validate import DISCOVERY_SERVICES
|
from ..services.validate import DISCOVERY_SERVICES
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -143,6 +144,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
|
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
|
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_LOGIN_BACKEND, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||||
vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
|
vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
|
||||||
vol.Required(ATTR_OPTIONS): dict,
|
vol.Required(ATTR_OPTIONS): dict,
|
||||||
@ -187,7 +189,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({
|
|||||||
SCHEMA_ADDON_USER = vol.Schema({
|
SCHEMA_ADDON_USER = vol.Schema({
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
|
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
|
||||||
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
|
vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
|
||||||
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
||||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_BOOT):
|
vol.Optional(ATTR_BOOT):
|
||||||
|
@ -5,6 +5,7 @@ from pathlib import Path
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
|
from .auth import APIAuth
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .homeassistant import APIHomeAssistant
|
from .homeassistant import APIHomeAssistant
|
||||||
from .hardware import APIHardware
|
from .hardware import APIHardware
|
||||||
@ -49,6 +50,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_discovery()
|
self._register_discovery()
|
||||||
self._register_services()
|
self._register_services()
|
||||||
self._register_info()
|
self._register_info()
|
||||||
|
self._register_auth()
|
||||||
|
|
||||||
def _register_host(self):
|
def _register_host(self):
|
||||||
"""Register hostcontrol functions."""
|
"""Register hostcontrol functions."""
|
||||||
@ -101,6 +103,15 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get('/info', api_info.info),
|
web.get('/info', api_info.info),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def _register_auth(self):
|
||||||
|
"""Register auth functions."""
|
||||||
|
api_auth = APIAuth()
|
||||||
|
api_auth.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes([
|
||||||
|
web.post('/auth', api_auth.auth),
|
||||||
|
])
|
||||||
|
|
||||||
def _register_supervisor(self):
|
def _register_supervisor(self):
|
||||||
"""Register Supervisor functions."""
|
"""Register Supervisor functions."""
|
||||||
api_supervisor = APISupervisor()
|
api_supervisor = APISupervisor()
|
||||||
|
58
hassio/api/auth.py
Normal file
58
hassio/api/auth.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Init file for Hass.io auth/SSO RESTful API."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import BasicAuth
|
||||||
|
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
|
||||||
|
|
||||||
|
from .utils import api_process
|
||||||
|
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, APIForbidden
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIAuth(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for auth functions."""
|
||||||
|
|
||||||
|
def _process_basic(self, request, addon):
|
||||||
|
"""Process login request with basic auth.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
|
||||||
|
return self.sys_auth.check_login(addon, auth.login, auth.password)
|
||||||
|
|
||||||
|
def _process_dict(self, request, addon, data):
|
||||||
|
"""Process login with dict data.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
username = data.get('username') or data.get('user')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
return self.sys_auth.check_login(addon, username, password)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def auth(self, request):
|
||||||
|
"""Process login request."""
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
|
if not addon.with_login_backend:
|
||||||
|
raise APIForbidden("Can't use Home Assistant auth!")
|
||||||
|
|
||||||
|
# BasicAuth
|
||||||
|
if AUTHORIZATION in request.headers:
|
||||||
|
return await self._process_basic(request, addon)
|
||||||
|
|
||||||
|
# Json
|
||||||
|
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
|
||||||
|
data = await request.json()
|
||||||
|
return await self._process_dict(request, addon, data)
|
||||||
|
|
||||||
|
# URL encoded
|
||||||
|
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_URL:
|
||||||
|
data = await request.post()
|
||||||
|
return await self._process_dict(request, addon, data)
|
||||||
|
|
||||||
|
raise APIError("Auth method not detected!")
|
@ -36,6 +36,7 @@ ADDONS_API_BYPASS = re.compile(
|
|||||||
r"|/info"
|
r"|/info"
|
||||||
r"|/services.*"
|
r"|/services.*"
|
||||||
r"|/discovery.*"
|
r"|/discovery.*"
|
||||||
|
r"|/auth"
|
||||||
r")$"
|
r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
91
hassio/auth.py
Normal file
91
hassio/auth.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
FILE_HASSIO_AUTH, ATTR_PASSWORD, ATTR_USERNAME, ATTR_ADDON)
|
||||||
|
from .coresys import CoreSysAttributes
|
||||||
|
from .utils.json import JsonConfig
|
||||||
|
from .validate import SCHEMA_AUTH_CONFIG
|
||||||
|
from .exceptions import AuthError, HomeAssistantAPIError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Auth(JsonConfig, CoreSysAttributes):
|
||||||
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
|
|
||||||
|
def __init__(self, coresys):
|
||||||
|
"""Initialize updater."""
|
||||||
|
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
def _check_cache(self, username, password):
|
||||||
|
"""Check password in cache."""
|
||||||
|
username_h = _rehash(username)
|
||||||
|
password_h = _rehash(password, username)
|
||||||
|
|
||||||
|
if self._data.get(username_h) == password_h:
|
||||||
|
_LOGGER.info("Cache hit for %s", username)
|
||||||
|
return True
|
||||||
|
|
||||||
|
_LOGGER.warning("No cache hit for %s", username)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _update_cache(self, username, password):
|
||||||
|
"""Cache a username, password."""
|
||||||
|
username_h = _rehash(username)
|
||||||
|
password_h = _rehash(password, username)
|
||||||
|
|
||||||
|
if self._data.get(username_h) == password_h:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._data[username_h] = password_h
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
def _dismatch_cache(self, username):
|
||||||
|
"""Remove user from cache."""
|
||||||
|
username_h = _rehash(username)
|
||||||
|
|
||||||
|
self._data.pop(username_h, None)
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
async def check_login(self, addon, username, password):
|
||||||
|
"""Check username login."""
|
||||||
|
if password is None:
|
||||||
|
_LOGGER.error("None as password is not supported!")
|
||||||
|
raise AuthError()
|
||||||
|
_LOGGER.info("Auth request from %s for %s", addon.slug, username)
|
||||||
|
|
||||||
|
# Check API state
|
||||||
|
if not await self.sys_homeassistant.check_api_state():
|
||||||
|
_LOGGER.info("Home Assistant not running, check cache")
|
||||||
|
return self._check_cache(username, password)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.sys_homeassistant.make_request(
|
||||||
|
'post', 'api/hassio_auth', json={
|
||||||
|
ATTR_USERNAME: username,
|
||||||
|
ATTR_PASSWORD: password,
|
||||||
|
ATTR_ADDON: addon.slug,
|
||||||
|
}) as req:
|
||||||
|
|
||||||
|
if req.status == 200:
|
||||||
|
_LOGGER.info("Success login from %s", username)
|
||||||
|
self._update_cache(username, password)
|
||||||
|
return True
|
||||||
|
|
||||||
|
_LOGGER.warning("Wrong login from %s", username)
|
||||||
|
self._dismatch_cache(username)
|
||||||
|
return False
|
||||||
|
except HomeAssistantAPIError:
|
||||||
|
_LOGGER.error("Can't request auth on Home Assistant!")
|
||||||
|
|
||||||
|
raise AuthError()
|
||||||
|
|
||||||
|
|
||||||
|
def _rehash(value, salt2=""):
|
||||||
|
"""Rehash a value."""
|
||||||
|
for idx in range(1, 20):
|
||||||
|
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
|
||||||
|
return value
|
@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
from colorlog import ColoredFormatter
|
from colorlog import ColoredFormatter
|
||||||
|
|
||||||
from .core import HassIO
|
from .core import HassIO
|
||||||
|
from .auth import Auth
|
||||||
from .addons import AddonManager
|
from .addons import AddonManager
|
||||||
from .api import RestAPI
|
from .api import RestAPI
|
||||||
from .const import SOCKET_DOCKER
|
from .const import SOCKET_DOCKER
|
||||||
@ -38,6 +39,7 @@ def initialize_coresys(loop):
|
|||||||
|
|
||||||
# Initialize core objects
|
# Initialize core objects
|
||||||
coresys.core = HassIO(coresys)
|
coresys.core = HassIO(coresys)
|
||||||
|
coresys.auth = Auth(coresys)
|
||||||
coresys.updater = Updater(coresys)
|
coresys.updater = Updater(coresys)
|
||||||
coresys.api = RestAPI(coresys)
|
coresys.api = RestAPI(coresys)
|
||||||
coresys.supervisor = Supervisor(coresys)
|
coresys.supervisor = Supervisor(coresys)
|
||||||
|
@ -16,6 +16,7 @@ URL_HASSOS_OTA = (
|
|||||||
|
|
||||||
HASSIO_DATA = Path("/data")
|
HASSIO_DATA = Path("/data")
|
||||||
|
|
||||||
|
FILE_HASSIO_AUTH = Path(HASSIO_DATA, "auth.json")
|
||||||
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
||||||
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
||||||
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
||||||
@ -50,6 +51,7 @@ CONTENT_TYPE_PNG = 'image/png'
|
|||||||
CONTENT_TYPE_JSON = 'application/json'
|
CONTENT_TYPE_JSON = 'application/json'
|
||||||
CONTENT_TYPE_TEXT = 'text/plain'
|
CONTENT_TYPE_TEXT = 'text/plain'
|
||||||
CONTENT_TYPE_TAR = 'application/tar'
|
CONTENT_TYPE_TAR = 'application/tar'
|
||||||
|
CONTENT_TYPE_URL = 'application/x-www-form-urlencoded'
|
||||||
HEADER_HA_ACCESS = 'x-ha-access'
|
HEADER_HA_ACCESS = 'x-ha-access'
|
||||||
HEADER_TOKEN = 'x-hassio-key'
|
HEADER_TOKEN = 'x-hassio-key'
|
||||||
|
|
||||||
@ -184,6 +186,7 @@ ATTR_PROTECTED = 'protected'
|
|||||||
ATTR_RATING = 'rating'
|
ATTR_RATING = 'rating'
|
||||||
ATTR_HASSIO_ROLE = 'hassio_role'
|
ATTR_HASSIO_ROLE = 'hassio_role'
|
||||||
ATTR_SUPERVISOR = 'supervisor'
|
ATTR_SUPERVISOR = 'supervisor'
|
||||||
|
ATTR_LOGIN_BACKEND = 'login_backend'
|
||||||
|
|
||||||
SERVICE_MQTT = 'mqtt'
|
SERVICE_MQTT = 'mqtt'
|
||||||
PROVIDE_SERVICE = 'provide'
|
PROVIDE_SERVICE = 'provide'
|
||||||
|
@ -33,6 +33,7 @@ class CoreSys:
|
|||||||
|
|
||||||
# Internal objects pointers
|
# Internal objects pointers
|
||||||
self._core = None
|
self._core = None
|
||||||
|
self._auth = None
|
||||||
self._homeassistant = None
|
self._homeassistant = None
|
||||||
self._supervisor = None
|
self._supervisor = None
|
||||||
self._addons = None
|
self._addons = None
|
||||||
@ -122,6 +123,18 @@ class CoreSys:
|
|||||||
raise RuntimeError("Hass.io already set!")
|
raise RuntimeError("Hass.io already set!")
|
||||||
self._core = value
|
self._core = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth(self):
|
||||||
|
"""Return Auth object."""
|
||||||
|
return self._auth
|
||||||
|
|
||||||
|
@auth.setter
|
||||||
|
def auth(self, value):
|
||||||
|
"""Set a Auth object."""
|
||||||
|
if self._auth:
|
||||||
|
raise RuntimeError("Auth already set!")
|
||||||
|
self._auth = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def homeassistant(self):
|
def homeassistant(self):
|
||||||
"""Return Home Assistant object."""
|
"""Return Home Assistant object."""
|
||||||
|
@ -57,6 +57,13 @@ class HassioUpdaterError(HassioError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
|
class AuthError(HassioError):
|
||||||
|
"""Auth errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
|
|
||||||
class HostError(HassioError):
|
class HostError(HassioError):
|
||||||
|
@ -442,9 +442,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
async with self.make_request('get', 'api/') as resp:
|
async with self.make_request('get', 'api/') as resp:
|
||||||
if resp.status in (200, 201):
|
if resp.status in (200, 201):
|
||||||
return True
|
return True
|
||||||
err = resp.status
|
status = resp.status
|
||||||
|
_LOGGER.warning("Home Assistant API config mismatch: %s", status)
|
||||||
|
|
||||||
_LOGGER.warning("Home Assistant API config mismatch: %d", err)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _block_till_run(self):
|
async def _block_till_run(self):
|
||||||
|
@ -23,6 +23,7 @@ DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
|
|||||||
ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
|
ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
|
||||||
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
|
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
|
||||||
UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$")
|
UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$")
|
||||||
|
SHA256 = vol.Match(r"^[0-9a-f]{64}$")
|
||||||
SERVICE_ALL = vol.In([SERVICE_MQTT])
|
SERVICE_ALL = vol.In([SERVICE_MQTT])
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ DOCKER_PORTS = vol.Schema({
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_HASS_CONFIG = vol.Schema({
|
SCHEMA_HASS_CONFIG = vol.Schema({
|
||||||
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
|
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
|
||||||
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
|
vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
|
||||||
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
|
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
|
||||||
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
|
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
|
||||||
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
|
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
|
||||||
@ -120,3 +121,8 @@ SCHEMA_DISCOVERY = vol.Schema([
|
|||||||
SCHEMA_DISCOVERY_CONFIG = vol.Schema({
|
SCHEMA_DISCOVERY_CONFIG = vol.Schema({
|
||||||
vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY),
|
vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY),
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_AUTH_CONFIG = vol.Schema({
|
||||||
|
SHA256: SHA256
|
||||||
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user