Add Permission checking for UniFi Protect (#73765)

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/73768/head
Christopher Bailey 2022-06-21 12:17:29 -04:00 committed by GitHub
parent a816348616
commit 3823edda32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 13 deletions

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin, T
from .models import PermRequired, ProtectSetableKeysMixin, T
@dataclass
@ -40,6 +40,7 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
device_class=ButtonDeviceClass.RESTART,
name="Reboot Device",
ufp_press="reboot",
ufp_perm=PermRequired.WRITE,
),
)
@ -49,6 +50,7 @@ SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
name="Clear Tamper",
icon="mdi:notification-clear-all",
ufp_press="clear_tamper",
ufp_perm=PermRequired.WRITE,
),
)

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
from .data import ProtectData
from .models import ProtectRequiredKeysMixin
from .models import PermRequired, ProtectRequiredKeysMixin
from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__)
@ -46,6 +46,11 @@ def _async_device_entities(
for device in data.get_by_types({model_type}):
assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime))
for description in descs:
if description.ufp_perm is not None:
can_write = device.can_write(data.api.bootstrap.auth_user)
if description.ufp_perm == PermRequired.WRITE and not can_write:
continue
if description.ufp_required_field:
required_field = get_nested_attr(device, description.ufp_required_field)
if not required_field:

View File

@ -25,13 +25,10 @@ async def async_setup_entry(
) -> None:
"""Set up lights for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities = [
ProtectLight(
data,
device,
)
for device in data.api.bootstrap.lights.values()
]
entities = []
for device in data.api.bootstrap.lights.values():
if device.can_write(data.api.bootstrap.auth_user):
entities.append(ProtectLight(data, device))
if not entities:
return

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from enum import Enum
import logging
from typing import Any, Generic, TypeVar
@ -17,6 +18,13 @@ _LOGGER = logging.getLogger(__name__)
T = TypeVar("T", bound=ProtectDeviceModel)
class PermRequired(int, Enum):
"""Type of permission level required for entity."""
NO_WRITE = 1
WRITE = 2
@dataclass
class ProtectRequiredKeysMixin(EntityDescription, Generic[T]):
"""Mixin for required keys."""
@ -25,6 +33,7 @@ class ProtectRequiredKeysMixin(EntityDescription, Generic[T]):
ufp_value: str | None = None
ufp_value_fn: Callable[[T], Any] | None = None
ufp_enabled: str | None = None
ufp_perm: PermRequired | None = None
def get_ufp_value(self, obj: T) -> Any:
"""Return value from UniFi Protect device."""

View File

@ -8,7 +8,7 @@ from pyunifiprotect.data import Camera, Doorlock, Light, ProtectModelWithId
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TIME_SECONDS
from homeassistant.const import PERCENTAGE, TIME_SECONDS
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin, T
from .models import PermRequired, ProtectSetableKeysMixin, T
@dataclass
@ -63,30 +63,35 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ufp_required_field="feature_flags.has_wdr",
ufp_value="isp_settings.wdr",
ufp_set_method="set_wdr_level",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="mic_level",
name="Microphone Level",
icon="mdi:microphone",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.has_mic",
ufp_value="mic_volume",
ufp_set_method="set_mic_volume",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="zoom_position",
name="Zoom Level",
icon="mdi:magnify-plus-outline",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.can_optical_zoom",
ufp_value="isp_settings.zoom_position",
ufp_set_method="set_camera_zoom",
ufp_perm=PermRequired.WRITE,
),
)
@ -96,12 +101,14 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
name="Motion Sensitivity",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field=None,
ufp_value="light_device_settings.pir_sensitivity",
ufp_set_method="set_sensitivity",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription[Light](
key="duration",
@ -115,6 +122,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ufp_required_field=None,
ufp_value_fn=_get_pir_duration,
ufp_set_method_fn=_set_pir_duration,
ufp_perm=PermRequired.WRITE,
),
)
@ -124,12 +132,14 @@ SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
name="Motion Sensitivity",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field=None,
ufp_value="motion_settings.sensitivity",
ufp_set_method="set_motion_sensitivity",
ufp_perm=PermRequired.WRITE,
),
)
@ -146,6 +156,7 @@ DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ufp_required_field=None,
ufp_value_fn=_get_auto_close,
ufp_set_method_fn=_set_auto_close,
ufp_perm=PermRequired.WRITE,
),
)
@ -155,11 +166,13 @@ CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
name="Volume",
icon="mdi:speaker",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_value="volume",
ufp_set_method="set_volume",
ufp_perm=PermRequired.WRITE,
),
)

View File

@ -38,7 +38,7 @@ from homeassistant.util.dt import utcnow
from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin, T
from .models import PermRequired, ProtectSetableKeysMixin, T
_LOGGER = logging.getLogger(__name__)
_KEY_LIGHT_MOTION = "light_motion"
@ -208,6 +208,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_enum_type=RecordingMode,
ufp_value="recording_settings.mode",
ufp_set_method="set_recording_mode",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription(
key="infrared",
@ -219,6 +220,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_enum_type=IRLEDMode,
ufp_value="isp_settings.ir_led_mode",
ufp_set_method="set_ir_led_model",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription[Camera](
key="doorbell_text",
@ -230,6 +232,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_value_fn=_get_doorbell_current,
ufp_options_fn=_get_doorbell_options,
ufp_set_method_fn=_set_doorbell_message,
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription(
key="chime_type",
@ -241,6 +244,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_enum_type=ChimeType,
ufp_value="chime_type",
ufp_set_method="set_chime_type",
ufp_perm=PermRequired.WRITE,
),
)
@ -253,6 +257,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_options=MOTION_MODE_TO_LIGHT_MODE,
ufp_value_fn=_get_light_motion_current,
ufp_set_method_fn=_set_light_mode,
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription[Light](
key="paired_camera",
@ -262,6 +267,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_value="camera_id",
ufp_options_fn=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
ufp_perm=PermRequired.WRITE,
),
)
@ -275,6 +281,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_enum_type=MountType,
ufp_value="mount_type",
ufp_set_method="set_mount_type",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription[Sensor](
key="paired_camera",
@ -284,6 +291,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_value="camera_id",
ufp_options_fn=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
ufp_perm=PermRequired.WRITE,
),
)
@ -296,6 +304,7 @@ DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_value="camera_id",
ufp_options_fn=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
ufp_perm=PermRequired.WRITE,
),
)
@ -308,6 +317,7 @@ VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ufp_options_fn=_get_viewer_options,
ufp_value_fn=_get_viewer_current,
ufp_set_method_fn=_set_liveview,
ufp_perm=PermRequired.WRITE,
),
)

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin, T
from .models import PermRequired, ProtectSetableKeysMixin, T
_LOGGER = logging.getLogger(__name__)
@ -56,6 +56,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="status_light",
@ -65,6 +66,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="feature_flags.has_led_status",
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="hdr_mode",
@ -74,6 +76,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="feature_flags.has_hdr",
ufp_value="hdr_mode",
ufp_set_method="set_hdr",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription[Camera](
key="high_fps",
@ -83,6 +86,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="feature_flags.has_highfps",
ufp_value_fn=_get_is_highfps,
ufp_set_method_fn=_set_highfps,
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key=_KEY_PRIVACY_MODE,
@ -91,6 +95,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_privacy_mask",
ufp_value="is_privacy_on",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="system_sounds",
@ -100,6 +105,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="feature_flags.has_speaker",
ufp_value="speaker_settings.are_system_sounds_enabled",
ufp_set_method="set_system_sounds",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_name",
@ -108,6 +114,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_name_enabled",
ufp_set_method="set_osd_name",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_date",
@ -116,6 +123,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_date_enabled",
ufp_set_method="set_osd_date",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_logo",
@ -124,6 +132,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_logo_enabled",
ufp_set_method="set_osd_logo",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_bitrate",
@ -132,6 +141,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_debug_enabled",
ufp_set_method="set_osd_bitrate",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="motion",
@ -140,6 +150,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="recording_settings.enable_motion_detection",
ufp_set_method="set_motion_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_person",
@ -149,6 +160,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="can_detect_person",
ufp_value="is_person_detection_on",
ufp_set_method="set_person_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_vehicle",
@ -158,6 +170,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="can_detect_vehicle",
ufp_value="is_vehicle_detection_on",
ufp_set_method="set_vehicle_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_face",
@ -167,6 +180,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="can_detect_face",
ufp_value="is_face_detection_on",
ufp_set_method="set_face_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_package",
@ -176,6 +190,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_required_field="can_detect_package",
ufp_value="is_package_detection_on",
ufp_set_method="set_package_detection",
ufp_perm=PermRequired.WRITE,
),
)
@ -187,6 +202,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="motion",
@ -195,6 +211,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="motion_settings.is_enabled",
ufp_set_method="set_motion_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="temperature",
@ -203,6 +220,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="temperature_settings.is_enabled",
ufp_set_method="set_temperature_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="humidity",
@ -211,6 +229,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="humidity_settings.is_enabled",
ufp_set_method="set_humidity_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="light",
@ -219,6 +238,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="light_settings.is_enabled",
ufp_set_method="set_light_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="alarm",
@ -226,6 +246,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="alarm_settings.is_enabled",
ufp_set_method="set_alarm_status",
ufp_perm=PermRequired.WRITE,
),
)
@ -239,6 +260,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="status_light",
@ -247,6 +269,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="light_device_settings.is_indicator_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
)
@ -258,6 +281,7 @@ DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
)
@ -270,6 +294,7 @@ VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
ufp_perm=PermRequired.WRITE,
),
)

View File

@ -8,6 +8,7 @@ import pytest
from pyunifiprotect.data import (
Camera,
Light,
Permission,
RecordingMode,
SmartDetectObjectType,
VideoMode,
@ -214,6 +215,40 @@ async def camera_privacy_fixture(
Camera.__config__.validate_assignment = True
async def test_switch_setup_no_perm(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_light: Light,
mock_camera: Camera,
):
"""Test switch entity setup for light devices."""
light_obj = mock_light.copy()
light_obj._api = mock_entry.api
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
mock_entry.api.bootstrap.auth_user.all_permissions = [
Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"})
]
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SWITCH, 0, 0)
async def test_switch_setup_light(
hass: HomeAssistant,
mock_entry: MockEntityFixture,