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
pull/15480/head
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):
"""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

View File

@ -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():

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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():

View File

@ -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

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
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', {