New apps Added
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Authorization helpers for write-capable WebSocket commands.
|
||||
|
||||
Home Assistant admins may always write. A non-admin user may write only when
|
||||
operator write delegation is switched on (``operator_write_enabled`` global
|
||||
option, default OFF) AND their user id is on the operator allowlist
|
||||
(``admin_panel_user_ids``), which an admin manages under Settings → Panel
|
||||
Access. With delegation off — the shipped default — content create / edit /
|
||||
delete is admin-only and the allowlist grants read-only operator access.
|
||||
|
||||
IMPORTANT — escalation boundary: ``require_write`` must be used ONLY on
|
||||
content-CRUD commands (object / task / group create-update-delete, user
|
||||
assignment, per-task analysis writes). Global-config, bulk-import and vacation
|
||||
commands keep ``@websocket_api.require_admin``, so an operator can never edit
|
||||
the allowlist nor flip the delegation switch (both live in the global options,
|
||||
gated by ``global/update``) — write access cannot be self-granted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||
from homeassistant.components.websocket_api.const import WebSocketCommandHandler
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
|
||||
from ..const import CONF_ADMIN_PANEL_USER_IDS, CONF_OPERATOR_WRITE_ENABLED
|
||||
from .global_options import get_global_options
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.auth.models import User
|
||||
|
||||
|
||||
def operator_user_ids(hass: HomeAssistant) -> list[str]:
|
||||
"""Return the operator allowlist (``admin_panel_user_ids``) as string ids."""
|
||||
raw = get_global_options(hass).get(CONF_ADMIN_PANEL_USER_IDS, []) or []
|
||||
return [uid for uid in raw if isinstance(uid, str)]
|
||||
|
||||
|
||||
def operator_write_enabled(hass: HomeAssistant) -> bool:
|
||||
"""Whether operator write delegation is switched on (default False).
|
||||
|
||||
Reads the ``operator_write_enabled`` global option. While False — the
|
||||
shipped default — only HA admins may write and the panel-access allowlist
|
||||
is read-only; an admin must explicitly enable this for allowlisted
|
||||
non-admins to gain content CRUD.
|
||||
"""
|
||||
return get_global_options(hass).get(CONF_OPERATOR_WRITE_ENABLED, False) is True
|
||||
|
||||
|
||||
def user_can_write(hass: HomeAssistant, user: User | None) -> bool:
|
||||
"""Whether a specific user may perform content writes.
|
||||
|
||||
True for HA admins. For non-admin users, true only when operator write
|
||||
delegation is enabled AND their id is on the operator allowlist. False for
|
||||
anonymous / missing users. This is the user-object variant used by the HTTP
|
||||
document views (which resolve ``request["hass_user"]``); the WS layer calls
|
||||
:func:`user_may_write`, which reads the user off the connection.
|
||||
"""
|
||||
if user is None:
|
||||
return False
|
||||
if user.is_admin:
|
||||
return True
|
||||
return operator_write_enabled(hass) and user.id in operator_user_ids(hass)
|
||||
|
||||
|
||||
def user_may_write(hass: HomeAssistant, connection: ActiveConnection) -> bool:
|
||||
"""Whether the connection's user may perform content writes.
|
||||
|
||||
True for HA admins. For non-admin users, true only when operator write
|
||||
delegation is enabled AND their id is on the operator allowlist. False for
|
||||
anonymous connections.
|
||||
"""
|
||||
return user_can_write(hass, connection.user)
|
||||
|
||||
|
||||
def require_write(func: WebSocketCommandHandler) -> WebSocketCommandHandler:
|
||||
"""Drop-in for ``@websocket_api.require_admin`` that also allows operators.
|
||||
|
||||
Mirrors HA's ``require_admin`` exactly (same decorator position: between
|
||||
``@websocket_command`` and ``@async_response``), but authorises any user for
|
||||
whom :func:`user_may_write` is true. Use ONLY on content-CRUD commands.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def with_write(hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]) -> None:
|
||||
"""Check write permission and call the wrapped handler."""
|
||||
if not user_may_write(hass, connection):
|
||||
raise Unauthorized
|
||||
|
||||
func(hass, connection, msg)
|
||||
|
||||
return with_write
|
||||
Reference in New Issue
Block a user