From ed0cfc4f31924ecf3f5a4a2cf7789e5c209563db Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <paulus@paulusschoutsen.nl> Date: Sun, 15 Jul 2018 20:46:15 +0200 Subject: [PATCH] Add user via cmd line creates owner (#15470) * Add user via cmd line creates owner * Ensure access tokens are not verified for inactive users * Stale print * Lint --- homeassistant/auth/__init__.py | 53 +++++++++++------- homeassistant/auth/auth_store.py | 10 ++++ homeassistant/components/auth/__init__.py | 6 +++ homeassistant/components/http/auth.py | 5 -- tests/auth/test_init.py | 4 +- tests/components/auth/test_init.py | 37 +++---------- tests/components/auth/test_init_link_user.py | 56 ++++---------------- tests/components/test_websocket_api.py | 27 ++++++++++ 8 files changed, 97 insertions(+), 101 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index fb35bd05c33..b05fca164a0 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -93,10 +93,15 @@ class AuthManager: async def async_create_user(self, name): """Create a user.""" - return await self._store.async_create_user( - name=name, - is_active=True, - ) + kwargs = { + 'name': name, + 'is_active': True, + } + + if await self._user_should_be_owner(): + kwargs['is_owner'] = True + + return await self._store.async_create_user(**kwargs) async def async_get_or_create_user(self, credentials): """Get or create a user.""" @@ -116,20 +121,10 @@ class AuthManager: info = await auth_provider.async_user_meta_for_credentials( credentials) - kwargs = { - 'credentials': credentials, - 'name': info.get('name') - } - - # Make owner and activate user if it's the first user. - if await self._store.async_get_users(): - kwargs['is_owner'] = False - kwargs['is_active'] = False - else: - kwargs['is_owner'] = True - kwargs['is_active'] = True - - return await self._store.async_create_user(**kwargs) + return await self._store.async_create_user( + credentials=credentials, + name=info.get('name'), + ) async def async_link_user(self, user, credentials): """Link credentials to an existing user.""" @@ -147,6 +142,14 @@ class AuthManager: await self._store.async_remove_user(user) + async def async_activate_user(self, user): + """Activate a user.""" + await self._store.async_activate_user(user) + + async def async_deactivate_user(self, user): + """Deactivate a user.""" + await self._store.async_deactivate_user(user) + async def async_remove_credentials(self, credentials): """Remove credentials.""" provider = self._async_get_auth_provider(credentials) @@ -191,7 +194,7 @@ class AuthManager: if tkn is None: return None - if tkn.expired: + if tkn.expired or not tkn.refresh_token.user.is_active: self._access_tokens.pop(token) return None @@ -218,3 +221,15 @@ class AuthManager: auth_provider_key = (credentials.auth_provider_type, credentials.auth_provider_id) return self._providers.get(auth_provider_key) + + async def _user_should_be_owner(self): + """Determine if user should be owner. + + A user should be an owner if it is the first non-system user that is + being created. + """ + for user in await self._store.async_get_users(): + if not user.system_generated: + return False + + return True diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index ebd61140ac1..8fd66d4bbb7 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -81,6 +81,16 @@ class AuthStore: self._users.pop(user.id) await self.async_save() + async def async_activate_user(self, user): + """Activate a user.""" + user.is_active = True + await self.async_save() + + async def async_deactivate_user(self, user): + """Activate a user.""" + user.is_active = False + await self.async_save() + async def async_remove_credentials(self, credentials): """Remove credentials.""" for user in self._users.values(): diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 1ead4cacdf0..f9588093933 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -275,6 +275,12 @@ class GrantTokenView(HomeAssistantView): }, status_code=400) user = await hass.auth.async_get_or_create_user(credentials) + + if not user.is_active: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + refresh_token = await hass.auth.async_create_refresh_token(user, client_id) access_token = hass.auth.async_create_access_token(refresh_token) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 46d77214160..2cc62dce38e 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -106,11 +106,6 @@ async def async_validate_auth_header(request, api_password=None): if access_token is None: return False - user = access_token.refresh_token.user - - if not user.is_active: - return False - request['hass_user'] = access_token.refresh_token.user return True diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index f7187fd49fd..3e3662c13c4 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -80,7 +80,7 @@ async def test_create_new_user(hass, hass_storage): credentials = step['result'] user = await manager.async_get_or_create_user(credentials) assert user is not None - assert user.is_owner is True + assert user.is_owner is False assert user.name == 'Test Name' @@ -198,7 +198,7 @@ async def test_saving_loading(hass, hass_storage): 'password': 'test-pass', }) user = await manager.async_get_or_create_user(step['result']) - + await manager.async_activate_user(user) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) manager.async_create_access_token(refresh_token) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index c5c46d55e39..59fc8714f77 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -10,7 +10,7 @@ from . import async_setup_auth from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI -async def test_login_new_user_and_refresh_token(hass, aiohttp_client): +async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" client = await async_setup_auth(hass, aiohttp_client, setup_api=True) resp = await client.post('/auth/login_flow', json={ @@ -34,36 +34,13 @@ async def test_login_new_user_and_refresh_token(hass, aiohttp_client): # Exchange code for tokens resp = await client.post('/auth/token', data={ - 'client_id': CLIENT_ID, - 'grant_type': 'authorization_code', - 'code': code - }) - - assert resp.status == 200 - tokens = await resp.json() - - assert hass.auth.async_get_access_token(tokens['access_token']) is not None - - # Use refresh token to get more tokens. - resp = await client.post('/auth/token', data={ - 'client_id': CLIENT_ID, - 'grant_type': 'refresh_token', - 'refresh_token': tokens['refresh_token'] - }) - - assert resp.status == 200 - tokens = await resp.json() - assert 'refresh_token' not in tokens - assert hass.auth.async_get_access_token(tokens['access_token']) is not None - - # Test using access token to hit API. - resp = await client.get('/api/') - assert resp.status == 401 - - resp = await client.get('/api/', headers={ - 'authorization': 'Bearer {}'.format(tokens['access_token']) + 'client_id': CLIENT_ID, + 'grant_type': 'authorization_code', + 'code': code }) - assert resp.status == 200 + + # User is not active + assert resp.status == 400 def test_credential_store_expiration(): diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 28a924bb43a..13515db87fa 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -25,40 +25,9 @@ async def async_get_code(hass, aiohttp_client): }] }] client = await async_setup_auth(hass, aiohttp_client, config) - - resp = await client.post('/auth/login_flow', json={ - 'client_id': CLIENT_ID, - 'handler': ['insecure_example', None], - 'redirect_uri': CLIENT_REDIRECT_URI, - }) - assert resp.status == 200 - step = await resp.json() - - resp = await client.post( - '/auth/login_flow/{}'.format(step['flow_id']), json={ - 'client_id': CLIENT_ID, - 'username': 'test-user', - 'password': 'test-pass', - }) - - assert resp.status == 200 - step = await resp.json() - code = step['result'] - - # Exchange code for tokens - resp = await client.post('/auth/token', data={ - 'client_id': CLIENT_ID, - 'grant_type': 'authorization_code', - 'code': code - }) - - assert resp.status == 200 - tokens = await resp.json() - - access_token = hass.auth.async_get_access_token(tokens['access_token']) - assert access_token is not None - user = access_token.refresh_token.user - assert len(user.credentials) == 1 + user = await hass.auth.async_create_user(name='Hello') + refresh_token = await hass.auth.async_create_refresh_token(user, CLIENT_ID) + access_token = hass.auth.async_create_access_token(refresh_token) # Now authenticate with the 2nd flow resp = await client.post('/auth/login_flow', json={ @@ -83,7 +52,7 @@ async def async_get_code(hass, aiohttp_client): 'user': user, 'code': step['result'], 'client': client, - 'tokens': tokens, + 'access_token': access_token.token, } @@ -92,18 +61,17 @@ async def test_link_user(hass, aiohttp_client): info = await async_get_code(hass, aiohttp_client) client = info['client'] code = info['code'] - tokens = info['tokens'] # Link user resp = await client.post('/auth/link_user', json={ 'client_id': CLIENT_ID, 'code': code }, headers={ - 'authorization': 'Bearer {}'.format(tokens['access_token']) + 'authorization': 'Bearer {}'.format(info['access_token']) }) assert resp.status == 200 - assert len(info['user'].credentials) == 2 + assert len(info['user'].credentials) == 1 async def test_link_user_invalid_client_id(hass, aiohttp_client): @@ -111,36 +79,34 @@ async def test_link_user_invalid_client_id(hass, aiohttp_client): info = await async_get_code(hass, aiohttp_client) client = info['client'] code = info['code'] - tokens = info['tokens'] # Link user resp = await client.post('/auth/link_user', json={ 'client_id': 'invalid', 'code': code }, headers={ - 'authorization': 'Bearer {}'.format(tokens['access_token']) + 'authorization': 'Bearer {}'.format(info['access_token']) }) assert resp.status == 400 - assert len(info['user'].credentials) == 1 + assert len(info['user'].credentials) == 0 async def test_link_user_invalid_code(hass, aiohttp_client): """Test linking a user to new credentials.""" info = await async_get_code(hass, aiohttp_client) client = info['client'] - tokens = info['tokens'] # Link user resp = await client.post('/auth/link_user', json={ 'client_id': CLIENT_ID, 'code': 'invalid' }, headers={ - 'authorization': 'Bearer {}'.format(tokens['access_token']) + 'authorization': 'Bearer {}'.format(info['access_token']) }) assert resp.status == 400 - assert len(info['user'].credentials) == 1 + assert len(info['user'].credentials) == 0 async def test_link_user_invalid_auth(hass, aiohttp_client): @@ -156,4 +122,4 @@ async def test_link_user_invalid_auth(hass, aiohttp_client): }, headers={'authorization': 'Bearer invalid'}) assert resp.status == 401 - assert len(info['user'].credentials) == 1 + assert len(info['user'].credentials) == 0 diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 6ea90bcdb88..dc1688bae16 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -341,6 +341,33 @@ async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): assert auth_msg['type'] == wapi.TYPE_AUTH_OK +async def test_auth_active_user_inactive(hass, aiohttp_client, + hass_access_token): + """Test authenticating with a token.""" + hass_access_token.refresh_token.user.is_active = False + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { -- GitLab