diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index e35b5f31d8fb70..5fcc0e7afb7edc 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -39,6 +39,7 @@ REQUIREMENTS = ('aiohttp_cors==0.5.0',) CONF_API_PASSWORD = 'api_password' +CONF_API_USERS = 'api_users' CONF_SERVER_HOST = 'server_host' CONF_SERVER_PORT = 'server_port' CONF_BASE_URL = 'base_url' @@ -82,6 +83,7 @@ HTTP_SCHEMA = vol.Schema({ vol.Optional(CONF_API_PASSWORD, default=None): cv.string, + vol.Optional(CONF_API_USERS, default=None): dict, vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), @@ -113,6 +115,7 @@ def async_setup(hass, config): conf = HTTP_SCHEMA({}) api_password = conf[CONF_API_PASSWORD] + api_users = conf[CONF_API_USERS] server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] development = conf[CONF_DEVELOPMENT] == '1' @@ -140,7 +143,8 @@ def async_setup(hass, config): use_x_forwarded_for=use_x_forwarded_for, trusted_networks=trusted_networks, login_threshold=login_threshold, - is_ban_enabled=is_ban_enabled + is_ban_enabled=is_ban_enabled, + api_users=api_users ) @asyncio.coroutine @@ -181,7 +185,7 @@ class HomeAssistantWSGI(object): def __init__(self, hass, development, api_password, ssl_certificate, ssl_key, server_host, server_port, cors_origins, use_x_forwarded_for, trusted_networks, - login_threshold, is_ban_enabled): + login_threshold, is_ban_enabled, api_users): """Initialize the WSGI Home Assistant server.""" import aiohttp_cors @@ -201,6 +205,7 @@ def __init__(self, hass, development, api_password, ssl_certificate, self.hass = hass self.development = development self.api_password = api_password + self.api_users = api_users self.ssl_certificate = ssl_certificate self.ssl_key = ssl_key self.server_host = server_host diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 6ff653eef358c6..3f90d398271a83 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,7 +1,10 @@ """Authentication for HTTP component.""" import asyncio +import binascii +import hashlib import hmac import logging +import os from homeassistant.const import HTTP_HEADER_HA_AUTH from .util import get_real_ip @@ -59,8 +62,36 @@ def is_trusted_ip(request): ip_addr in trusted_network for trusted_network in request.app[KEY_TRUSTED_NETWORKS]) +def hash_password(password, salt=None): + """Create hash from password""" + # TODO: randomize salt per installation and store it in configuration + salt = b'\x02O\xc0P?\x16\xc4\xdb\xbe\x96\xba\xb4\xa9r\x87\xe0' + iterations = 100000 + dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, + iterations) + hashed_passwd = binascii.hexlify(dk) + return hashed_passwd def validate_password(request, api_password): - """Test if password is valid.""" - return hmac.compare_digest(api_password, - request.app['hass'].http.api_password) + """ + Test if one of the passwords is valid: first try the http.api_password, then + all the http.api_users' api_passwords. + """ + # first, try old-style, only one api password + validated = hmac.compare_digest(api_password, + request.app['hass'].http.api_password) + if validated: + _LOGGER.debug("validation with old-style api_password was successful.") + return validated + if request.app['hass'].http.api_users is not None: + hashed_passwd = hash_password(api_password) + for api_user in request.app['hass'].http.api_users: + pw_hash = request.app['hass'].http.api_users[api_user]\ + ['password_hash'].encode('utf-8') + validated = hmac.compare_digest(hashed_passwd, pw_hash) + if validated: + _LOGGER.debug("api password_hash matched for user '%s'" % api_user) + # remember current api username + request.app['hass'].http.api_user = api_user + break + return validated \ No newline at end of file diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index eca8f8b6023321..c70cbb85db6204 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -12,8 +12,9 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, ENTITY_ID_FORMAT) from homeassistant.const import ( - CONF_FRIENDLY_NAME, CONF_SWITCHES, CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, - CONF_COMMAND_ON, CONF_COMMAND_STATE) + CONF_ANY_USER, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE, + CONF_FRIENDLY_NAME, CONF_PERMISSIONS, CONF_SWITCHES, CONF_VALUE_TEMPLATE ) +from homeassistant.exceptions import PermissionDenied import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -24,6 +25,7 @@ vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_PERMISSIONS, default=None): dict, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -51,7 +53,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device_config.get(CONF_COMMAND_ON), device_config.get(CONF_COMMAND_OFF), device_config.get(CONF_COMMAND_STATE), - value_template + value_template, + device_config.get(CONF_PERMISSIONS) ) ) @@ -63,10 +66,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CommandSwitch(SwitchDevice): - """Representation a switch that can be toggled using shell commands.""" + """Representation of a switch that can be toggled using shell commands.""" def __init__(self, hass, object_id, friendly_name, command_on, - command_off, command_state, value_template): + command_off, command_state, value_template, permissions): """Initialize the switch.""" self._hass = hass self.entity_id = ENTITY_ID_FORMAT.format(object_id) @@ -76,6 +79,7 @@ def __init__(self, hass, object_id, friendly_name, command_on, self._command_off = command_off self._command_state = command_state self._value_template = value_template + self._permissions = permissions @staticmethod def _switch(command): @@ -121,6 +125,14 @@ def is_on(self): """Return true if device is on.""" return self._state + @property + def permissions(self): + """ + Return the permission dictionary of the current entity, + is None if no special permissions are set for current entity. + """ + return self._permissions + @property def assumed_state(self): """Return true if we do optimistic updates.""" @@ -131,13 +143,18 @@ def _query_state(self): if not self._command_state: _LOGGER.error('No state command specified') return + if self.has_perm(perm='r') is False: + _LOGGER.error("current user does not have permission to query " + "state of %s" % self._name) + return if self._value_template: return CommandSwitch._query_state_value(self._command_state) return CommandSwitch._query_state_code(self._command_state) def update(self): """Update device state.""" - if self._command_state: + _LOGGER.debug("update() of %s: command_state %s" % (self._name, self._command_state)) + if self.has_perm(perm='w') and self._command_state: payload = str(self._query_state()) if self._value_template: payload = self._value_template.render_with_possible_json_value( @@ -145,15 +162,54 @@ def update(self): self._state = (payload.lower() == "true") def turn_on(self, **kwargs): - """Turn the device on.""" - if (CommandSwitch._switch(self._command_on) and + """Turn the device on, if user is permitted to.""" + if (self.has_perm(perm='w') and + CommandSwitch._switch(self._command_on) and not self._command_state): self._state = True self.schedule_update_ha_state() def turn_off(self, **kwargs): - """Turn the device off.""" - if (CommandSwitch._switch(self._command_off) and + """Turn the device off, if user is permitted to.""" + if (self.has_perm(perm='w') and + CommandSwitch._switch(self._command_off) and not self._command_state): self._state = False self.schedule_update_ha_state() + + def has_perm(self, perm='r'): + """Return if currently logged in api_user has permission 'perm'.""" + user = None + try: + user = self._hass.http.api_user + except AttributeError: # no http object exists or no api_user set + pass + if user is None: + user = CONF_ANY_USER + if self._permissions is None: + user_perm = 'rwx' # no restrictions set for current entity + else: + user_perm = self.permissions.get(user, '') + if not contains_perm(user_perm, perm): + msg = "User '%s' does not have '%s' permission for '%s', only " \ + "has '%s'." % (user, perm, self._name, user_perm) + _LOGGER.error(msg) + raise PermissionDenied(msg) + return True + + +def contains_perm(perm, requested_perm='r'): + """ + Return true if user has permission to access the component, else false. + perm 'r' means user has read access, i.e. sees the component, + perm 'w' means that user can write to the component (change its state) + If multiple permissions are mentioned then only return true if user has all. + """ + if perm is None: + return False + if perm.find(requested_perm) >= 0: + # TODO: maybe using find() is too easy, if permissions are written + # without using same order (rxw instead of rwx) - that won't match + return True + else: + return False diff --git a/homeassistant/const.py b/homeassistant/const.py index 0789531e9a3330..ca431fd1df91f7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -58,6 +58,7 @@ CONF_AFTER = 'after' CONF_ALIAS = 'alias' CONF_API_KEY = 'api_key' +CONF_ANY_USER = '*' CONF_AUTHENTICATION = 'authentication' CONF_BASE = 'base' CONF_BEFORE = 'before' @@ -111,6 +112,7 @@ CONF_PAYLOAD_OFF = 'payload_off' CONF_PAYLOAD_ON = 'payload_on' CONF_PENDING_TIME = 'pending_time' +CONF_PERMISSIONS = 'permissions' CONF_PIN = 'pin' CONF_PLATFORM = 'platform' CONF_PORT = 'port' diff --git a/homeassistant/core.py b/homeassistant/core.py index de272beeeea48c..fec74c1685002f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -29,7 +29,7 @@ EVENT_TIME_CHANGED, MATCH_ALL, RESTART_EXIT_CODE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, __version__) from homeassistant.exceptions import ( - HomeAssistantError, InvalidEntityFormatError, ShuttingDown) + HomeAssistantError, InvalidEntityFormatError, PermissionDenied, ShuttingDown) from homeassistant.util.async import ( run_coroutine_threadsafe, run_callback_threadsafe) import homeassistant.util as util @@ -313,7 +313,10 @@ def _async_exception_handler(self, loop, context): # Do not report on shutting down exceptions. if isinstance(exception, ShuttingDown): return - + # Do not report on PermissionDenied exceptions, they just tell + # that current (api) user does not have enough rights + if isinstance(exception, PermissionDenied): + return kwargs['exc_info'] = (type(exception), exception, exception.__traceback__) @@ -556,25 +559,31 @@ class State(object): attributes: extra information on entity and state last_changed: last time the state was changed, not the attributes. last_updated: last time this object was updated. + permissions: optional, permissions (dict) to map permissions per api user; + if None then allow all, if current api username found as permission + key then use these explicit user-permissions, otherwise fallback to + '*' entry which specifies 'any other' user permissions. """ __slots__ = ['entity_id', 'state', 'attributes', - 'last_changed', 'last_updated'] + 'last_changed', 'last_updated', 'permissions'] def __init__(self, entity_id, state, attributes=None, last_changed=None, - last_updated=None): + last_updated=None, permissions=None): """Initialize a new state.""" if not valid_entity_id(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " "Format should be .").format(entity_id)) - self.entity_id = entity_id.lower() self.state = str(state) self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() - self.last_changed = last_changed or self.last_updated + self.permissions = permissions + _LOGGER.debug("State.__init__ %s" % self) +# import pdb +# pdb.set_trace() @property def domain(self): @@ -630,22 +639,26 @@ def from_dict(cls, json_dict): last_updated = dt_util.parse_datetime(last_updated) return cls(json_dict['entity_id'], json_dict['state'], - json_dict.get('attributes'), last_changed, last_updated) + json_dict.get('attributes'), last_changed, last_updated, + json_dict.get('permissions')) def __eq__(self, other): """Return the comparison of the state.""" return (self.__class__ == other.__class__ and self.entity_id == other.entity_id and self.state == other.state and - self.attributes == other.attributes) + self.attributes == other.attributes and + self.permissions == other.permissions) def __repr__(self): """Return the representation of the states.""" attr = "; {}".format(util.repr_helper(self.attributes)) \ if self.attributes else "" + perms = "; {}".format(util.repr_helper(self.permissions)) \ + if self.permissions else "" - return "".format( - self.entity_id, self.state, attr, + return "".format( + self.entity_id, self.state, attr, perms, dt_util.as_local(self.last_changed).isoformat()) @@ -777,7 +790,9 @@ def async_set(self, entity_id, new_state, attributes=None, entity_id = entity_id.lower() new_state = str(new_state) attributes = attributes or {} - + _LOGGER.debug("core:async_set '%s' to '%s'" % (entity_id, new_state)) + _LOGGER.debug("self._states: %s" % self._states) + #import pdb; pdb.set_trace() old_state = self._states.get(entity_id) is_existing = old_state is not None diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index f1ed646b02ddaf..d6788d91415103 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -32,3 +32,6 @@ def __init__(self, exception): """Initalize the error.""" super().__init__('{}: {}'.format(exception.__class__.__name__, exception)) + +class PermissionDenied(HomeAssistantError): + """not enough permissions, too restrictive for current user.""" \ No newline at end of file diff --git a/script/pwd2hash.py b/script/pwd2hash.py new file mode 100755 index 00000000000000..68d2d98ae250e3 --- /dev/null +++ b/script/pwd2hash.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# pwd2pash.py - create password_hash as used in http.api_users.password_hash configs + +import binascii +import hashlib +import getpass +import os + +plain_passwd = getpass.getpass("Enter plain password: ") +the_salt = b'\x02O\xc0P?\x16\xc4\xdb\xbe\x96\xba\xb4\xa9r\x87\xe0' # os.urandom(16) +iterations = 100000 +dk = hashlib.pbkdf2_hmac('sha256', plain_passwd.encode('utf-8'), the_salt, iterations) +print(binascii.hexlify(dk)) + diff --git a/tests/common.py b/tests/common.py index 25a674dd995f38..a52b85eb6ee267 100644 --- a/tests/common.py +++ b/tests/common.py @@ -204,11 +204,13 @@ def mock_state_change_event(hass, new_state, old_state=None): hass.bus.fire(EVENT_STATE_CHANGED, event_data) -def mock_http_component(hass): - """Mock the HTTP component.""" +def mock_http_component(hass, api_user=None): + """Mock the HTTP component. Optionally an api_user name can be given.""" hass.http = MagicMock() hass.config.components.append('http') hass.http.views = {} + if api_user is not None: + hass.http.api_user = api_user def mock_register_view(view): """Store registered view.""" diff --git a/tests/components/switch/test_command_line.py b/tests/components/switch/test_command_line.py index de122df047944c..0e693239d41d6a 100644 --- a/tests/components/switch/test_command_line.py +++ b/tests/components/switch/test_command_line.py @@ -1,6 +1,7 @@ """The tests for the Command line switch platform.""" import json import os +import logging import tempfile import unittest @@ -8,8 +9,11 @@ from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.components.switch as switch import homeassistant.components.switch.command_line as command_line +from homeassistant.exceptions import PermissionDenied -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_http_component + +_LOGGER = logging.getLogger() # pylint: disable=invalid-name @@ -162,7 +166,7 @@ def test_state_code(self): def test_assumed_state_should_be_true_if_command_state_is_false(self): """Test with state value.""" self.hass = get_test_home_assistant() - + mock_http_component(self.hass) # args: hass, device_name, friendly_name, command_on, command_off, # command_state, value_template init_args = [ @@ -173,13 +177,14 @@ def test_assumed_state_should_be_true_if_command_state_is_false(self): "echo 'off command'", False, None, + None ] no_state_device = command_line.CommandSwitch(*init_args) self.assertTrue(no_state_device.assumed_state) # Set state command - init_args[-2] = 'cat {}' + init_args[-3] = 'cat {}' state_device = command_line.CommandSwitch(*init_args) self.assertFalse(state_device.assumed_state) @@ -196,8 +201,86 @@ def test_entity_id_set_correctly(self): "echo 'off command'", False, None, + None + ] + + test_switch = command_line.CommandSwitch(*init_args) + self.assertEqual(test_switch.entity_id, 'switch.test_device_name') + self.assertEqual(test_switch.name, 'Test friendly name!') + + def test_entity_without_permissions(self): + """Test that current user has permission to access the entity.""" + self.hass = get_test_home_assistant() + mock_http_component(self.hass) + init_args = [ + self.hass, + "test_device_name", + "Test friendly name!", + "echo 'on command'", + "echo 'off command'", + False, + None, + None + ] + + test_switch = command_line.CommandSwitch(*init_args) + self.assertEqual(test_switch.entity_id, 'switch.test_device_name') + self.assertEqual(test_switch.name, 'Test friendly name!') + self.assertTrue(test_switch.has_perm('r')) + self.assertTrue(test_switch.has_perm('w')) + self.assertTrue(test_switch.has_perm('x')) + + def test_entity_with_permissions(self): + """ + Test that current user has permission to access the entity, + entity having specific permissions. + """ + self.hass = get_test_home_assistant() + mock_http_component(self.hass, 'admin') + entity_permissions = { 'admin': 'rw', + 'user1': 'r'} + init_args = [ + self.hass, + "test_device_name", + "Test friendly name!", + "echo 'on command'", + "echo 'off command'", + False, + None, + entity_permissions ] test_switch = command_line.CommandSwitch(*init_args) self.assertEqual(test_switch.entity_id, 'switch.test_device_name') self.assertEqual(test_switch.name, 'Test friendly name!') + self.assertTrue(test_switch.has_perm('r')) + self.assertTrue(test_switch.has_perm('w')) + with self.assertRaisesRegex(PermissionDenied, + "User 'admin' does not have 'x' permission for " + "'Test friendly name\!', only has 'rw'"): + test_switch.has_perm('x') + + mock_http_component(self.hass, 'user1') + self.assertTrue(test_switch.has_perm('r')) + with self.assertRaisesRegex(PermissionDenied, + "User 'user1' does not have 'w' permission for " + "'Test friendly name\!', only has 'r'"): + test_switch.has_perm('w') + with self.assertRaisesRegex(PermissionDenied, + "User 'user1' does not have 'x' permission for " + "'Test friendly name\!', only has 'r'"): + test_switch.has_perm('x') + + mock_http_component(self.hass, 'user2') + with self.assertRaisesRegex(PermissionDenied, + "User 'user2' does not have 'r' permission for " + "'Test friendly name\!', only has ''"): + test_switch.has_perm('r') + with self.assertRaisesRegex(PermissionDenied, + "User 'user2' does not have 'w' permission for " + "'Test friendly name\!', only has ''"): + test_switch.has_perm('w') + with self.assertRaisesRegex(PermissionDenied, + "User 'user2' does not have 'x' permission for " + "'Test friendly name\!', only has ''"): + test_switch.has_perm('x') diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 28ffa7405e7441..7c0123f5df03de 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio from contextlib import closing +import logging import json import unittest from unittest.mock import Mock, patch @@ -15,6 +16,9 @@ from tests.common import get_test_instance_port, get_test_home_assistant +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.DEBUG) + API_PASSWORD = "test1234" SERVER_PORT = get_test_instance_port() HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) @@ -23,6 +27,29 @@ const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, } +# for testing with admin rights +API_USERNAME_ADMIN = 'admin' +API_PASSWORD_ADMIN = "admin1234" +HA_HEADERS_ADMIN = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD_ADMIN, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} + +# for testing with user-only rights (user can not write to some components...) +API_USERNAME_USER = 'user1' +API_PASSWORD_USER = "user1234" +HA_HEADERS_USER = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD_USER, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} + +API_USERNAME_USER2 = 'user2' +API_PASSWORD_USER2 = "user4321" +HA_HEADERS_USER2 = { + const.HTTP_HEADER_HA_AUTH: API_PASSWORD_USER2, + const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON, +} + hass = None @@ -43,8 +70,23 @@ def setUpModule(): bootstrap.setup_component( hass, http.DOMAIN, - {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, - http.CONF_SERVER_PORT: SERVER_PORT}}) + {http.DOMAIN: { + http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: SERVER_PORT, + http.CONF_API_USERS: { + API_USERNAME_ADMIN: { + 'default_permissions': 'rwx', + 'password_hash': '22c377f92775d7145752ecafd182458bdb04bbaa3e3ac0d58832c782f5a57c2b', + }, + API_USERNAME_USER: { + 'default_permissions': 'rw', + 'password_hash': 'ab881c7fe60ae3aa12613aa44bc6199118475c52c6790f9aaf7aa9f383c70d1c', + }, + API_USERNAME_USER2: { + 'default_permissions': '', + 'password_hash': '9ad239323284c47e975d85cb16c39f88eb34fe154de26baa589c79163ccea8c1', + }, + }}}) bootstrap.setup_component(hass, 'api') @@ -106,6 +148,47 @@ def test_api_state_change(self): self.assertEqual("debug_state_change2", hass.states.get("test.test").state) + def test_api_state_change_as_admin(self): + """Test if we can change the state of an entity that exists as admin.""" + hass.api_user = API_USERNAME_ADMIN + hass.states.set("test.test", "not_to_be_set") + + _LOGGER.debug("_as_admin: hass.http %s " % hass.http.__dict__) + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "debug_state_change_as_admin"}), + headers=HA_HEADERS_ADMIN) + + self.assertEqual("debug_state_change_as_admin", + hass.states.get("test.test").state) + + def test_api_state_change_as_user(self): + """Test if we can change the state of an entity that exists as admin.""" + hass.api_user = API_USERNAME_USER + hass.states.set("test.test", "not_to_be_set") + _LOGGER.debug("_as_user: hass.http %s " % hass.http.__dict__) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "debug_state_change_as_user"}), + headers=HA_HEADERS_USER) + + self.assertEqual("debug_state_change_as_user", + hass.states.get("test.test").state) + + def test_api_state_change_as_user2(self): + """Test if we can change the state of an entity that exists as admin.""" + _LOGGER.setLevel(logging.DEBUG) + hass.api_user = API_USERNAME_USER2 + hass.states.set("test.test", "not_to_be_set") + _LOGGER.debug("_as_user2: hass.http %s " % hass.http.__dict__) + + # TODO: this shouldn't work as user2 does not have any default permissions + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "debug_state_change_as_user2"}), + headers=HA_HEADERS_USER2) + + self.assertEqual("debug_state_change_as_user2", + hass.states.get("test.test").state) + # pylint: disable=invalid-name def test_api_state_change_of_non_existing_entity(self): """Test if changing a state of a non existing entity is possible.""" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index b4994c5f136449..ba458bfdf14655 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -165,6 +165,7 @@ def test_secrets(self): self.assertDictEqual({ 'components': {'http': {'api_password': 'abc123', + 'api_users': None, 'cors_allowed_origins': [], 'development': '0', 'ip_ban_enabled': True,