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:
parent
6db069881b
commit
ed0cfc4f31
8 changed files with 97 additions and 101 deletions
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue