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
This commit is contained in:
Paulus Schoutsen 2018-07-15 20:46:15 +02:00 committed by GitHub
parent 6db069881b
commit ed0cfc4f31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 97 additions and 101 deletions

View file

@ -93,10 +93,15 @@ class AuthManager:
async def async_create_user(self, name): async def async_create_user(self, name):
"""Create a user.""" """Create a user."""
return await self._store.async_create_user( kwargs = {
name=name, 'name': name,
is_active=True, '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): async def async_get_or_create_user(self, credentials):
"""Get or create a user.""" """Get or create a user."""
@ -116,20 +121,10 @@ class AuthManager:
info = await auth_provider.async_user_meta_for_credentials( info = await auth_provider.async_user_meta_for_credentials(
credentials) credentials)
kwargs = { return await self._store.async_create_user(
'credentials': credentials, credentials=credentials,
'name': info.get('name') 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)
async def async_link_user(self, user, credentials): async def async_link_user(self, user, credentials):
"""Link credentials to an existing user.""" """Link credentials to an existing user."""
@ -147,6 +142,14 @@ class AuthManager:
await self._store.async_remove_user(user) 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): async def async_remove_credentials(self, credentials):
"""Remove credentials.""" """Remove credentials."""
provider = self._async_get_auth_provider(credentials) provider = self._async_get_auth_provider(credentials)
@ -191,7 +194,7 @@ class AuthManager:
if tkn is None: if tkn is None:
return None return None
if tkn.expired: if tkn.expired or not tkn.refresh_token.user.is_active:
self._access_tokens.pop(token) self._access_tokens.pop(token)
return None return None
@ -218,3 +221,15 @@ class AuthManager:
auth_provider_key = (credentials.auth_provider_type, auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id) credentials.auth_provider_id)
return self._providers.get(auth_provider_key) 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

View file

@ -81,6 +81,16 @@ class AuthStore:
self._users.pop(user.id) self._users.pop(user.id)
await self.async_save() 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): async def async_remove_credentials(self, credentials):
"""Remove credentials.""" """Remove credentials."""
for user in self._users.values(): for user in self._users.values():

View file

@ -275,6 +275,12 @@ class GrantTokenView(HomeAssistantView):
}, status_code=400) }, status_code=400)
user = await hass.auth.async_get_or_create_user(credentials) 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, refresh_token = await hass.auth.async_create_refresh_token(user,
client_id) client_id)
access_token = hass.auth.async_create_access_token(refresh_token) access_token = hass.auth.async_create_access_token(refresh_token)

View file

@ -106,11 +106,6 @@ async def async_validate_auth_header(request, api_password=None):
if access_token is None: if access_token is None:
return False return False
user = access_token.refresh_token.user
if not user.is_active:
return False
request['hass_user'] = access_token.refresh_token.user request['hass_user'] = access_token.refresh_token.user
return True return True

View file

@ -80,7 +80,7 @@ async def test_create_new_user(hass, hass_storage):
credentials = step['result'] credentials = step['result']
user = await manager.async_get_or_create_user(credentials) user = await manager.async_get_or_create_user(credentials)
assert user is not None assert user is not None
assert user.is_owner is True assert user.is_owner is False
assert user.name == 'Test Name' assert user.name == 'Test Name'
@ -198,7 +198,7 @@ async def test_saving_loading(hass, hass_storage):
'password': 'test-pass', 'password': 'test-pass',
}) })
user = await manager.async_get_or_create_user(step['result']) 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) refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID)
manager.async_create_access_token(refresh_token) manager.async_create_access_token(refresh_token)

View file

@ -10,7 +10,7 @@ from . import async_setup_auth
from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI 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.""" """Test logging in with new user and refreshing tokens."""
client = await async_setup_auth(hass, aiohttp_client, setup_api=True) client = await async_setup_auth(hass, aiohttp_client, setup_api=True)
resp = await client.post('/auth/login_flow', json={ 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 # Exchange code for tokens
resp = await client.post('/auth/token', data={ resp = await client.post('/auth/token', data={
'client_id': CLIENT_ID, 'client_id': CLIENT_ID,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': 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'])
}) })
assert resp.status == 200
# User is not active
assert resp.status == 400
def test_credential_store_expiration(): def test_credential_store_expiration():

View file

@ -25,40 +25,9 @@ async def async_get_code(hass, aiohttp_client):
}] }]
}] }]
client = await async_setup_auth(hass, aiohttp_client, config) client = await async_setup_auth(hass, aiohttp_client, config)
user = await hass.auth.async_create_user(name='Hello')
resp = await client.post('/auth/login_flow', json={ refresh_token = await hass.auth.async_create_refresh_token(user, CLIENT_ID)
'client_id': CLIENT_ID, access_token = hass.auth.async_create_access_token(refresh_token)
'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
# Now authenticate with the 2nd flow # Now authenticate with the 2nd flow
resp = await client.post('/auth/login_flow', json={ resp = await client.post('/auth/login_flow', json={
@ -83,7 +52,7 @@ async def async_get_code(hass, aiohttp_client):
'user': user, 'user': user,
'code': step['result'], 'code': step['result'],
'client': client, '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) info = await async_get_code(hass, aiohttp_client)
client = info['client'] client = info['client']
code = info['code'] code = info['code']
tokens = info['tokens']
# Link user # Link user
resp = await client.post('/auth/link_user', json={ resp = await client.post('/auth/link_user', json={
'client_id': CLIENT_ID, 'client_id': CLIENT_ID,
'code': code 'code': code
}, headers={ }, headers={
'authorization': 'Bearer {}'.format(tokens['access_token']) 'authorization': 'Bearer {}'.format(info['access_token'])
}) })
assert resp.status == 200 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): 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) info = await async_get_code(hass, aiohttp_client)
client = info['client'] client = info['client']
code = info['code'] code = info['code']
tokens = info['tokens']
# Link user # Link user
resp = await client.post('/auth/link_user', json={ resp = await client.post('/auth/link_user', json={
'client_id': 'invalid', 'client_id': 'invalid',
'code': code 'code': code
}, headers={ }, headers={
'authorization': 'Bearer {}'.format(tokens['access_token']) 'authorization': 'Bearer {}'.format(info['access_token'])
}) })
assert resp.status == 400 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): async def test_link_user_invalid_code(hass, aiohttp_client):
"""Test linking a user to new credentials.""" """Test linking a user to new credentials."""
info = await async_get_code(hass, aiohttp_client) info = await async_get_code(hass, aiohttp_client)
client = info['client'] client = info['client']
tokens = info['tokens']
# Link user # Link user
resp = await client.post('/auth/link_user', json={ resp = await client.post('/auth/link_user', json={
'client_id': CLIENT_ID, 'client_id': CLIENT_ID,
'code': 'invalid' 'code': 'invalid'
}, headers={ }, headers={
'authorization': 'Bearer {}'.format(tokens['access_token']) 'authorization': 'Bearer {}'.format(info['access_token'])
}) })
assert resp.status == 400 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): 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'}) }, headers={'authorization': 'Bearer invalid'})
assert resp.status == 401 assert resp.status == 401
assert len(info['user'].credentials) == 1 assert len(info['user'].credentials) == 0

View file

@ -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 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): async def test_auth_active_with_password_not_allow(hass, aiohttp_client):
"""Test authenticating with a token.""" """Test authenticating with a token."""
assert await async_setup_component(hass, 'websocket_api', { assert await async_setup_component(hass, 'websocket_api', {