mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
Replace api_password in Camera.Push (#16339)
* Use access_token and user provided token instead of api_password * address comments by @awarecan * new tests * add extra checks and test * lint * add comment
This commit is contained in:
parent
6b08e6e769
commit
20f6cb7cc7
@ -13,8 +13,10 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
|
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
|
||||||
STATE_IDLE, STATE_RECORDING
|
STATE_IDLE, STATE_RECORDING
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.http.view import HomeAssistantView
|
from homeassistant.components.http.view import KEY_AUTHENTICATED,\
|
||||||
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST
|
HomeAssistantView
|
||||||
|
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
|
||||||
|
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
@ -25,11 +27,13 @@ DEPENDENCIES = ['http']
|
|||||||
|
|
||||||
CONF_BUFFER_SIZE = 'buffer'
|
CONF_BUFFER_SIZE = 'buffer'
|
||||||
CONF_IMAGE_FIELD = 'field'
|
CONF_IMAGE_FIELD = 'field'
|
||||||
|
CONF_TOKEN = 'token'
|
||||||
|
|
||||||
DEFAULT_NAME = "Push Camera"
|
DEFAULT_NAME = "Push Camera"
|
||||||
|
|
||||||
ATTR_FILENAME = 'filename'
|
ATTR_FILENAME = 'filename'
|
||||||
ATTR_LAST_TRIP = 'last_trip'
|
ATTR_LAST_TRIP = 'last_trip'
|
||||||
|
ATTR_TOKEN = 'token'
|
||||||
|
|
||||||
PUSH_CAMERA_DATA = 'push_camera'
|
PUSH_CAMERA_DATA = 'push_camera'
|
||||||
|
|
||||||
@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
|
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
|
||||||
cv.time_period, cv.positive_timedelta),
|
cv.time_period, cv.positive_timedelta),
|
||||||
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
|
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
|
||||||
|
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||||||
|
|
||||||
cameras = [PushCamera(config[CONF_NAME],
|
cameras = [PushCamera(config[CONF_NAME],
|
||||||
config[CONF_BUFFER_SIZE],
|
config[CONF_BUFFER_SIZE],
|
||||||
config[CONF_TIMEOUT])]
|
config[CONF_TIMEOUT],
|
||||||
|
config.get(CONF_TOKEN))]
|
||||||
|
|
||||||
hass.http.register_view(CameraPushReceiver(hass,
|
hass.http.register_view(CameraPushReceiver(hass,
|
||||||
config[CONF_IMAGE_FIELD]))
|
config[CONF_IMAGE_FIELD]))
|
||||||
@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView):
|
|||||||
|
|
||||||
url = "/api/camera_push/{entity_id}"
|
url = "/api/camera_push/{entity_id}"
|
||||||
name = 'api:camera_push:camera_entity'
|
name = 'api:camera_push:camera_entity'
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, hass, image_field):
|
def __init__(self, hass, image_field):
|
||||||
"""Initialize CameraPushReceiver with camera entity."""
|
"""Initialize CameraPushReceiver with camera entity."""
|
||||||
@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView):
|
|||||||
|
|
||||||
if _camera is None:
|
if _camera is None:
|
||||||
_LOGGER.error("Unknown %s", entity_id)
|
_LOGGER.error("Unknown %s", entity_id)
|
||||||
|
status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\
|
||||||
|
else HTTP_UNAUTHORIZED
|
||||||
return self.json_message('Unknown {}'.format(entity_id),
|
return self.json_message('Unknown {}'.format(entity_id),
|
||||||
HTTP_BAD_REQUEST)
|
status)
|
||||||
|
|
||||||
|
# Supports HA authentication and token based
|
||||||
|
# when token has been configured
|
||||||
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
|
(_camera.token is not None and
|
||||||
|
request.query.get('token') == _camera.token))
|
||||||
|
|
||||||
|
if not authenticated:
|
||||||
|
return self.json_message(
|
||||||
|
'Invalid authorization credentials for {}'.format(entity_id),
|
||||||
|
HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await request.post()
|
data = await request.post()
|
||||||
@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView):
|
|||||||
class PushCamera(Camera):
|
class PushCamera(Camera):
|
||||||
"""The representation of a Push camera."""
|
"""The representation of a Push camera."""
|
||||||
|
|
||||||
def __init__(self, name, buffer_size, timeout):
|
def __init__(self, name, buffer_size, timeout, token):
|
||||||
"""Initialize push camera component."""
|
"""Initialize push camera component."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -106,6 +126,7 @@ class PushCamera(Camera):
|
|||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self.queue = deque([], buffer_size)
|
self.queue = deque([], buffer_size)
|
||||||
self._current_image = None
|
self._current_image = None
|
||||||
|
self.token = token
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
@ -168,5 +189,6 @@ class PushCamera(Camera):
|
|||||||
name: value for name, value in (
|
name: value for name, value in (
|
||||||
(ATTR_LAST_TRIP, self._last_trip),
|
(ATTR_LAST_TRIP, self._last_trip),
|
||||||
(ATTR_FILENAME, self._filename),
|
(ATTR_FILENAME, self._filename),
|
||||||
|
(ATTR_TOKEN, self.token),
|
||||||
) if value is not None
|
) if value is not None
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ from datetime import timedelta
|
|||||||
from homeassistant import core as ha
|
from homeassistant import core as ha
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from tests.components.auth import async_setup_auth
|
from homeassistant.components.http.auth import setup_auth
|
||||||
|
|
||||||
|
|
||||||
async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
|
async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
|
||||||
@ -15,19 +15,69 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
|
|||||||
'camera': {
|
'camera': {
|
||||||
'platform': 'push',
|
'platform': 'push',
|
||||||
'name': 'config_test',
|
'name': 'config_test',
|
||||||
|
'token': '12345678'
|
||||||
}})
|
}})
|
||||||
|
client = await aiohttp_client(hass.http.app)
|
||||||
client = await async_setup_auth(hass, aiohttp_client)
|
|
||||||
|
|
||||||
# missing file
|
# missing file
|
||||||
resp = await client.post('/api/camera_push/camera.config_test')
|
resp = await client.post('/api/camera_push/camera.config_test')
|
||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
|
|
||||||
files = {'image': io.BytesIO(b'fake')}
|
|
||||||
|
|
||||||
# wrong entity
|
# wrong entity
|
||||||
|
files = {'image': io.BytesIO(b'fake')}
|
||||||
resp = await client.post('/api/camera_push/camera.wrong', data=files)
|
resp = await client.post('/api/camera_push/camera.wrong', data=files)
|
||||||
assert resp.status == 400
|
assert resp.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cases_with_no_auth(aioclient_mock, hass, aiohttp_client):
|
||||||
|
"""Test cases where aiohttp_client is not auth."""
|
||||||
|
await async_setup_component(hass, 'camera', {
|
||||||
|
'camera': {
|
||||||
|
'platform': 'push',
|
||||||
|
'name': 'config_test',
|
||||||
|
'token': '12345678'
|
||||||
|
}})
|
||||||
|
|
||||||
|
setup_auth(hass.http.app, [], True, api_password=None)
|
||||||
|
client = await aiohttp_client(hass.http.app)
|
||||||
|
|
||||||
|
# wrong token
|
||||||
|
files = {'image': io.BytesIO(b'fake')}
|
||||||
|
resp = await client.post('/api/camera_push/camera.config_test?token=1234',
|
||||||
|
data=files)
|
||||||
|
assert resp.status == 401
|
||||||
|
|
||||||
|
# right token
|
||||||
|
files = {'image': io.BytesIO(b'fake')}
|
||||||
|
resp = await client.post(
|
||||||
|
'/api/camera_push/camera.config_test?token=12345678',
|
||||||
|
data=files)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_auth_no_token(aioclient_mock, hass, aiohttp_client):
|
||||||
|
"""Test cases where aiohttp_client is not auth."""
|
||||||
|
await async_setup_component(hass, 'camera', {
|
||||||
|
'camera': {
|
||||||
|
'platform': 'push',
|
||||||
|
'name': 'config_test',
|
||||||
|
}})
|
||||||
|
|
||||||
|
setup_auth(hass.http.app, [], True, api_password=None)
|
||||||
|
client = await aiohttp_client(hass.http.app)
|
||||||
|
|
||||||
|
# no token
|
||||||
|
files = {'image': io.BytesIO(b'fake')}
|
||||||
|
resp = await client.post('/api/camera_push/camera.config_test',
|
||||||
|
data=files)
|
||||||
|
assert resp.status == 401
|
||||||
|
|
||||||
|
# fake token
|
||||||
|
files = {'image': io.BytesIO(b'fake')}
|
||||||
|
resp = await client.post(
|
||||||
|
'/api/camera_push/camera.config_test?token=12345678',
|
||||||
|
data=files)
|
||||||
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
||||||
async def test_posting_url(hass, aiohttp_client):
|
async def test_posting_url(hass, aiohttp_client):
|
||||||
@ -36,6 +86,7 @@ async def test_posting_url(hass, aiohttp_client):
|
|||||||
'camera': {
|
'camera': {
|
||||||
'platform': 'push',
|
'platform': 'push',
|
||||||
'name': 'config_test',
|
'name': 'config_test',
|
||||||
|
'token': '12345678'
|
||||||
}})
|
}})
|
||||||
|
|
||||||
client = await aiohttp_client(hass.http.app)
|
client = await aiohttp_client(hass.http.app)
|
||||||
@ -46,7 +97,9 @@ async def test_posting_url(hass, aiohttp_client):
|
|||||||
assert camera_state.state == 'idle'
|
assert camera_state.state == 'idle'
|
||||||
|
|
||||||
# post image
|
# post image
|
||||||
resp = await client.post('/api/camera_push/camera.config_test', data=files)
|
resp = await client.post(
|
||||||
|
'/api/camera_push/camera.config_test?token=12345678',
|
||||||
|
data=files)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
||||||
# state recording
|
# state recording
|
||||||
|
Loading…
x
Reference in New Issue
Block a user