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:
Diogo Gomes 2018-09-11 10:30:20 +01:00 committed by Paulus Schoutsen
parent 6b08e6e769
commit 20f6cb7cc7
2 changed files with 87 additions and 12 deletions

View File

@ -13,8 +13,10 @@ import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST
from homeassistant.components.http.view import KEY_AUTHENTICATED,\
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.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
@ -25,11 +27,13 @@ DEPENDENCIES = ['http']
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'
DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'
PUSH_CAMERA_DATA = 'push_camera'
@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta),
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],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])]
config[CONF_TIMEOUT],
config.get(CONF_TOKEN))]
hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView):
url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
requires_auth = False
def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity."""
@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView):
if _camera is None:
_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),
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:
data = await request.post()
@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView):
class PushCamera(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."""
super().__init__()
self._name = name
@ -106,6 +126,7 @@ class PushCamera(Camera):
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
self.token = token
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
@ -168,5 +189,6 @@ class PushCamera(Camera):
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None
}

View File

@ -6,7 +6,7 @@ from datetime import timedelta
from homeassistant import core as ha
from homeassistant.setup import async_setup_component
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):
@ -15,19 +15,69 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
'camera': {
'platform': 'push',
'name': 'config_test',
'token': '12345678'
}})
client = await async_setup_auth(hass, aiohttp_client)
client = await aiohttp_client(hass.http.app)
# missing file
resp = await client.post('/api/camera_push/camera.config_test')
assert resp.status == 400
files = {'image': io.BytesIO(b'fake')}
# wrong entity
files = {'image': io.BytesIO(b'fake')}
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):
@ -36,6 +86,7 @@ async def test_posting_url(hass, aiohttp_client):
'camera': {
'platform': 'push',
'name': 'config_test',
'token': '12345678'
}})
client = await aiohttp_client(hass.http.app)
@ -46,7 +97,9 @@ async def test_posting_url(hass, aiohttp_client):
assert camera_state.state == 'idle'
# 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
# state recording