From 8735b681815d9cf184ebc98c53f5272b3583e7bf Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Tue, 17 Jul 2018 22:01:51 -0700 Subject: [PATCH 1/5] Added WS endpoint for changing homeassistant password. --- homeassistant/auth/providers/homeassistant.py | 8 +++ .../config/auth_provider_homeassistant.py | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index b359f67d77fd1..0bc28078a566f 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -199,6 +199,14 @@ async def async_will_remove_credentials(self, credentials): # Can happen if somehow we didn't clean up a credential pass + async def async_change_password(self, username, new_password): + """Helper to change a user's password.""" + if self.data is None: + await self.async_initialize() + + await self.hass.async_add_executor_job( + self.data.change_password, username, new_password) + class LoginFlow(data_entry_flow.FlowHandler): """Handler for the login flow.""" diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index fca03ad8fa9f0..f14f4e89637a9 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -20,6 +20,13 @@ vol.Required('username'): str, }) +WS_TYPE_CHANGE_PASSWORD = 'config/auth_provider/homeassistant/change_password' +SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_CHANGE_PASSWORD, + vol.Required('current_password'): str, + vol.Required('new_password'): str +}) + async def async_setup(hass): """Enable the Home Assistant views.""" @@ -31,6 +38,10 @@ async def async_setup(hass): WS_TYPE_DELETE, websocket_delete, SCHEMA_WS_DELETE ) + hass.components.websocket_api.async_register_command( + WS_TYPE_CHANGE_PASSWORD, websocket_change_password, + SCHEMA_WS_CHANGE_PASSWORD + ) return True @@ -118,3 +129,44 @@ async def delete_creds(): websocket_api.result_message(msg['id'])) hass.async_add_job(delete_creds()) + + +@callback +def websocket_change_password(hass, connection, msg): + """Change user password.""" + async def change_password(): + """Change user password.""" + provider = _get_provider(hass) + await provider.async_initialize() + + user = connection.request.get('hass_user') + if user is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'user_not_found', 'User not found')) + return + + username = None + for credential in user.credentials: + if credential.auth_provider_type == provider.type: + username = credential.data['username'] + + if username is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'credentials_not_found', 'Credentials not found')) + return + + try: + await provider.async_validate_login( + username, msg['current_password']) + except auth_ha.InvalidAuth: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'invalid_password', 'Invalid password')) + return + + await provider.async_change_password(username, msg['new_password']) + await provider.data.async_save() + + connection.to_write.put_nowait( + websocket_api.result_message(msg['id'])) + + hass.async_add_job(change_password()) From a519049fbcb84f972d4b5df44c068a144e93aa0d Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Wed, 18 Jul 2018 13:12:51 -0700 Subject: [PATCH 2/5] Remove change password helper. Don't require current password. --- homeassistant/auth/providers/homeassistant.py | 8 -------- .../config/auth_provider_homeassistant.py | 19 ++++++------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 0bc28078a566f..b359f67d77fd1 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -199,14 +199,6 @@ async def async_will_remove_credentials(self, credentials): # Can happen if somehow we didn't clean up a credential pass - async def async_change_password(self, username, new_password): - """Helper to change a user's password.""" - if self.data is None: - await self.async_initialize() - - await self.hass.async_add_executor_job( - self.data.change_password, username, new_password) - class LoginFlow(data_entry_flow.FlowHandler): """Handler for the login flow.""" diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index f14f4e89637a9..ee7e44b1b6b98 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -23,7 +23,6 @@ WS_TYPE_CHANGE_PASSWORD = 'config/auth_provider/homeassistant/change_password' SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_CHANGE_PASSWORD, - vol.Required('current_password'): str, vol.Required('new_password'): str }) @@ -136,34 +135,28 @@ def websocket_change_password(hass, connection, msg): """Change user password.""" async def change_password(): """Change user password.""" - provider = _get_provider(hass) - await provider.async_initialize() - user = connection.request.get('hass_user') if user is None: connection.send_message_outside(websocket_api.error_message( msg['id'], 'user_not_found', 'User not found')) return + provider = _get_provider(hass) + await provider.async_initialize() + username = None for credential in user.credentials: if credential.auth_provider_type == provider.type: username = credential.data['username'] + break if username is None: connection.send_message_outside(websocket_api.error_message( msg['id'], 'credentials_not_found', 'Credentials not found')) return - try: - await provider.async_validate_login( - username, msg['current_password']) - except auth_ha.InvalidAuth: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'invalid_password', 'Invalid password')) - return - - await provider.async_change_password(username, msg['new_password']) + await hass.async_add_executor_job( + provider.data.change_password, username, msg['new_password']) await provider.data.async_save() connection.to_write.put_nowait( From 48d0b1017486833fcf71171e57f7632550300fbf Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Wed, 18 Jul 2018 14:38:14 -0700 Subject: [PATCH 3/5] Restore current password verification. --- .../components/config/auth_provider_homeassistant.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index ee7e44b1b6b98..6b2fdb7d436a1 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -23,6 +23,7 @@ WS_TYPE_CHANGE_PASSWORD = 'config/auth_provider/homeassistant/change_password' SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_CHANGE_PASSWORD, + vol.Required('current_password'): str, vol.Required('new_password'): str }) @@ -155,6 +156,14 @@ async def change_password(): msg['id'], 'credentials_not_found', 'Credentials not found')) return + try: + await provider.async_validate_login( + username, msg['current_password']) + except auth_ha.InvalidAuth: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'invalid_password', 'Invalid password')) + return + await hass.async_add_executor_job( provider.data.change_password, username, msg['new_password']) await provider.data.async_save() From 1953594a95d79220adbdd24a36bdfb08280ab705 Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Wed, 18 Jul 2018 20:20:45 -0700 Subject: [PATCH 4/5] Added tests. --- .../test_auth_provider_homeassistant.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index fa4ab612bb12c..cd2cbc4453996 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -227,3 +227,77 @@ async def test_delete_unknown_auth(hass, hass_ws_client, hass_access_token): result = await client.receive_json() assert not result['success'], result assert result['error']['code'] == 'auth_not_found' + + +async def test_change_password(hass, hass_ws_client, hass_access_token): + """Test that change password succeeds with valid password.""" + provider = hass.auth.auth_providers[0] + await provider.async_initialize() + await hass.async_add_executor_job( + provider.data.add_auth, 'test-user', 'test-pass') + + credentials = await provider.async_get_or_create_credentials({ + 'username': 'test-user' + }) + + user = hass_access_token.refresh_token.user + await hass.auth.async_link_user(user, credentials) + + client = await hass_ws_client(hass, hass_access_token) + await client.send_json({ + 'id': 6, + 'type': auth_ha.WS_TYPE_CHANGE_PASSWORD, + 'current_password': 'test-pass', + 'new_password': 'new-pass' + }) + + result = await client.receive_json() + assert result['success'], result + await provider.async_validate_login('test-user', 'new-pass') + + +async def test_change_password_wrong_pw(hass, hass_ws_client, + hass_access_token): + """Test that change password fails with invalid password.""" + provider = hass.auth.auth_providers[0] + await provider.async_initialize() + await hass.async_add_executor_job( + provider.data.add_auth, 'test-user', 'test-pass') + + credentials = await provider.async_get_or_create_credentials({ + 'username': 'test-user' + }) + + user = hass_access_token.refresh_token.user + await hass.auth.async_link_user(user, credentials) + + client = await hass_ws_client(hass, hass_access_token) + await client.send_json({ + 'id': 6, + 'type': auth_ha.WS_TYPE_CHANGE_PASSWORD, + 'current_password': 'wrong-pass', + 'new_password': 'new-pass' + }) + + result = await client.receive_json() + assert not result['success'], result + assert result['error']['code'] == 'invalid_password' + with pytest.raises(prov_ha.InvalidAuth): + await provider.async_validate_login('test-user', 'new-pass') + + +async def test_change_password_no_creds(hass, hass_ws_client, + hass_access_token): + """Test that change password fails with no credentials.""" + client = await hass_ws_client(hass, hass_access_token) + + await client.send_json({ + 'id': 6, + 'type': auth_ha.WS_TYPE_CHANGE_PASSWORD, + 'current_password': 'test-pass', + 'new_password': 'new-pass' + }) + + result = await client.receive_json() + assert not result['success'], result + assert result['error']['code'] == 'credentials_not_found' From 093d593f3a98458d9138557eaba1fef994ac4ec5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Jul 2018 09:16:05 +0200 Subject: [PATCH 5/5] Use correct send method --- homeassistant/components/config/auth_provider_homeassistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 6b2fdb7d436a1..960e8f5e7b4e4 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -168,7 +168,7 @@ async def change_password(): provider.data.change_password, username, msg['new_password']) await provider.data.async_save() - connection.to_write.put_nowait( + connection.send_message_outside( websocket_api.result_message(msg['id'])) hass.async_add_job(change_password())