diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 0e943b33fb8..ae6abf04c02 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -45,6 +45,7 @@ def setup_auth(app, trusted_networks, use_auth, support_legacy=False, api_password=None): """Create auth middleware for the app.""" old_auth_warning = set() + legacy_auth = (not use_auth or support_legacy) and api_password @middleware async def auth_middleware(request, handler): @@ -60,7 +61,6 @@ def setup_auth(app, trusted_networks, use_auth, request.path, request[KEY_REAL_IP]) old_auth_warning.add(request.path) - legacy_auth = (not use_auth or support_legacy) and api_password if (hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header( request, api_password if legacy_auth else None)): @@ -91,6 +91,11 @@ def setup_auth(app, trusted_networks, use_auth, app['hass']) elif _is_trusted_ip(request, trusted_networks): + users = await app['hass'].auth.async_get_users() + for user in users: + if user.is_owner: + request['hass_user'] = user + break authenticated = True elif not use_auth and api_password is None: @@ -136,8 +141,9 @@ async def async_validate_auth_header(request, api_password=None): # If no space in authorization header return False + hass = request.app['hass'] + if auth_type == 'Bearer': - hass = request.app['hass'] refresh_token = await hass.auth.async_validate_access_token(auth_val) if refresh_token is None: return False @@ -157,8 +163,12 @@ async def async_validate_auth_header(request, api_password=None): if username != 'homeassistant': return False - return hmac.compare_digest(api_password.encode('utf-8'), - password.encode('utf-8')) + if not hmac.compare_digest(api_password.encode('utf-8'), + password.encode('utf-8')): + return False + + request['hass_user'] = await legacy_api_password.async_get_user(hass) + return True return False diff --git a/homeassistant/components/light/fibaro.py b/homeassistant/components/light/fibaro.py index 96069d50335..7157dcfd31b 100644 --- a/homeassistant/components/light/fibaro.py +++ b/homeassistant/components/light/fibaro.py @@ -65,7 +65,8 @@ class FibaroLight(FibaroDevice, Light): self._update_lock = asyncio.Lock() if 'levelChange' in fibaro_device.interfaces: self._supported_flags |= SUPPORT_BRIGHTNESS - if 'color' in fibaro_device.properties: + if 'color' in fibaro_device.properties and \ + 'setColor' in fibaro_device.actions: self._supported_flags |= SUPPORT_COLOR if 'setW' in fibaro_device.actions: self._supported_flags |= SUPPORT_WHITE_VALUE @@ -168,7 +169,9 @@ class FibaroLight(FibaroDevice, Light): if self._supported_flags & SUPPORT_BRIGHTNESS: self._brightness = float(self.fibaro_device.properties.value) # Color handling - if self._supported_flags & SUPPORT_COLOR: + if self._supported_flags & SUPPORT_COLOR and \ + 'color' in self.fibaro_device.properties and \ + ',' in self.fibaro_device.properties.color: # Fibaro communicates the color as an 'R, G, B, W' string rgbw_s = self.fibaro_device.properties.color if rgbw_s == '0,0,0,0' and\ diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d91ab6ee445..bf83b173972 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -208,11 +208,8 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, if value[-1] == TOPIC_BASE and key.endswith('_topic'): payload[key] = "{}{}".format(value[:-1], base) - # If present, unique_id is used as the discovered object id. Otherwise, - # if present, the node_id will be included in the discovered object id - discovery_id = payload.get( - 'unique_id', ' '.join( - (node_id, object_id)) if node_id else object_id) + # If present, the node_id will be included in the discovered object id + discovery_id = ' '.join((node_id, object_id)) if node_id else object_id discovery_hash = (component, discovery_id) if payload: diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 5e1da2595af..0d449083f72 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -17,7 +17,7 @@ import yarl from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['gTTS-token==1.1.2'] +REQUIREMENTS = ['gTTS-token==1.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6b69609be22..1a1d45396f0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 83 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/requirements_all.txt b/requirements_all.txt index dc0f7e8679f..e4020ca41e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -406,7 +406,7 @@ freesms==0.1.2 fritzhome==1.0.4 # homeassistant.components.tts.google -gTTS-token==1.1.2 +gTTS-token==1.1.3 # homeassistant.components.sensor.gearbest gearbest_parser==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7223771891..f602c04bd79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ feedparser==5.2.1 foobot_async==0.3.1 # homeassistant.components.tts.google -gTTS-token==1.1.2 +gTTS-token==1.1.3 # homeassistant.components.geo_location.geo_json_events # homeassistant.components.geo_location.nsw_rural_fire_service_feed diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 88bd39ebfe2..71d179211a2 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -333,39 +333,6 @@ async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): assert state is None -async def test_discovery_unique_id(hass, mqtt_mock, caplog): - """Test unique id option only creates one sensor per unique_id.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - await async_start(hass, 'homeassistant', {}, entry) - data1 = ( - '{ "name": "Beer",' - ' "state_topic": "test_topic",' - ' "unique_id": "TOTALLY_UNIQUE" }' - ) - data2 = ( - '{ "name": "Milk",' - ' "state_topic": "test_topic",' - ' "unique_id": "TOTALLY_DIFFERENT" }' - ) - async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', - data1) - await hass.async_block_till_done() - state = hass.states.get('binary_sensor.beer') - assert state is not None - assert state.name == 'Beer' - async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', - data2) - await hass.async_block_till_done() - await hass.async_block_till_done() - state = hass.states.get('binary_sensor.beer') - assert state is not None - assert state.name == 'Beer' - - state = hass.states.get('binary_sensor.milk') - assert state is not None - assert state.name == 'Milk' - - async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT binary sensor device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 110ba8d5ad6..d3cbdba63b4 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -88,6 +88,12 @@ def hass_access_token(hass, hass_admin_user): yield hass.auth.async_create_access_token(refresh_token) +@pytest.fixture +def hass_owner_user(hass, local_auth): + """Return a Home Assistant admin user.""" + return MockUser(is_owner=True).add_to_hass(hass) + + @pytest.fixture def hass_admin_user(hass, local_auth): """Return a Home Assistant admin user.""" diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 979bfc28689..222e8ced6e7 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -7,6 +7,7 @@ import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized +from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.auth import setup_auth, async_sign_path from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip @@ -84,29 +85,40 @@ async def test_access_without_password(app, aiohttp_client): async def test_access_with_password_in_header(app, aiohttp_client, - legacy_auth): + legacy_auth, hass): """Test access with password in header.""" setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) + user = await legacy_api_password.async_get_user(hass) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': None, + 'user_id': user.id, + } req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: 'wrong-pass'}) assert req.status == 401 -async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth): +async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, + hass): """Test access with password in URL.""" setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) + user = await legacy_api_password.async_get_user(hass) resp = await client.get('/', params={ 'api_password': API_PASSWORD }) assert resp.status == 200 + assert await resp.json() == { + 'refresh_token_id': None, + 'user_id': user.id, + } resp = await client.get('/') assert resp.status == 401 @@ -117,15 +129,20 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth): assert resp.status == 401 -async def test_basic_auth_works(app, aiohttp_client): +async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): """Test access with basic authentication.""" setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) + user = await legacy_api_password.async_get_user(hass) req = await client.get( '/', auth=BasicAuth('homeassistant', API_PASSWORD)) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': None, + 'user_id': user.id, + } req = await client.get( '/', @@ -145,7 +162,7 @@ async def test_basic_auth_works(app, aiohttp_client): assert req.status == 401 -async def test_access_with_trusted_ip(app2, aiohttp_client): +async def test_access_with_trusted_ip(app2, aiohttp_client, hass_owner_user): """Test access with an untrusted ip address.""" setup_auth(app2, TRUSTED_NETWORKS, False, api_password='some-pass') @@ -163,6 +180,10 @@ async def test_access_with_trusted_ip(app2, aiohttp_client): resp = await client.get('/') assert resp.status == 200, \ "{} should be trusted".format(remote_addr) + assert await resp.json() == { + 'refresh_token_id': None, + 'user_id': hass_owner_user.id, + } async def test_auth_active_access_with_access_token_in_header( @@ -171,18 +192,32 @@ async def test_auth_active_access_with_access_token_in_header( token = hass_access_token setup_auth(app, [], True, api_password=None) client = await aiohttp_client(app) + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) req = await client.get( '/', headers={'Authorization': 'Bearer {}'.format(token)}) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': refresh_token.id, + 'user_id': refresh_token.user.id, + } req = await client.get( '/', headers={'AUTHORIZATION': 'Bearer {}'.format(token)}) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': refresh_token.id, + 'user_id': refresh_token.user.id, + } req = await client.get( '/', headers={'authorization': 'Bearer {}'.format(token)}) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': refresh_token.id, + 'user_id': refresh_token.user.id, + } req = await client.get( '/', headers={'Authorization': token}) @@ -200,7 +235,8 @@ async def test_auth_active_access_with_access_token_in_header( assert req.status == 401 -async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client): +async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client, + hass_owner_user): """Test access with an untrusted ip address.""" setup_auth(app2, TRUSTED_NETWORKS, True, api_password=None) @@ -218,6 +254,10 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client): resp = await client.get('/') assert resp.status == 200, \ "{} should be trusted".format(remote_addr) + assert await resp.json() == { + 'refresh_token_id': None, + 'user_id': hass_owner_user.id, + } async def test_auth_active_blocked_api_password_access( @@ -242,24 +282,37 @@ async def test_auth_active_blocked_api_password_access( async def test_auth_legacy_support_api_password_access( - app, aiohttp_client, legacy_auth): + app, aiohttp_client, legacy_auth, hass): """Test access using api_password if auth.support_legacy.""" setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD) client = await aiohttp_client(app) + user = await legacy_api_password.async_get_user(hass) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': None, + 'user_id': user.id, + } resp = await client.get('/', params={ 'api_password': API_PASSWORD }) assert resp.status == 200 + assert await resp.json() == { + 'refresh_token_id': None, + 'user_id': user.id, + } req = await client.get( '/', auth=BasicAuth('homeassistant', API_PASSWORD)) assert req.status == 200 + assert await req.json() == { + 'refresh_token_id': None, + 'user_id': user.id, + } async def test_auth_access_signed_path(