2018-07-13 09:43:08 +00:00
|
|
|
"""Provide an authentication layer for Home Assistant."""
|
|
|
|
import asyncio
|
|
|
|
import logging
|
|
|
|
from collections import OrderedDict
|
2018-07-31 14:00:17 +00:00
|
|
|
from typing import List, Awaitable
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
from homeassistant import data_entry_flow
|
2018-07-31 14:00:17 +00:00
|
|
|
from homeassistant.core import callback, HomeAssistant
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
from . import models
|
|
|
|
from . import auth_store
|
|
|
|
from .providers import auth_provider_from_config
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2018-07-31 14:00:17 +00:00
|
|
|
async def auth_manager_from_config(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
provider_configs: List[dict]) -> Awaitable['AuthManager']:
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Initialize an auth manager from config."""
|
|
|
|
store = auth_store.AuthStore(hass)
|
|
|
|
if provider_configs:
|
|
|
|
providers = await asyncio.gather(
|
|
|
|
*[auth_provider_from_config(hass, store, config)
|
|
|
|
for config in provider_configs])
|
|
|
|
else:
|
|
|
|
providers = []
|
|
|
|
# So returned auth providers are in same order as config
|
|
|
|
provider_hash = OrderedDict()
|
|
|
|
for provider in providers:
|
|
|
|
if provider is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
key = (provider.type, provider.id)
|
|
|
|
|
|
|
|
if key in provider_hash:
|
|
|
|
_LOGGER.error(
|
|
|
|
'Found duplicate provider: %s. Please add unique IDs if you '
|
|
|
|
'want to have the same provider twice.', key)
|
|
|
|
continue
|
|
|
|
|
|
|
|
provider_hash[key] = provider
|
|
|
|
manager = AuthManager(hass, store, provider_hash)
|
|
|
|
return manager
|
|
|
|
|
|
|
|
|
|
|
|
class AuthManager:
|
|
|
|
"""Manage the authentication for Home Assistant."""
|
|
|
|
|
|
|
|
def __init__(self, hass, store, providers):
|
|
|
|
"""Initialize the auth manager."""
|
|
|
|
self._store = store
|
|
|
|
self._providers = providers
|
|
|
|
self.login_flow = data_entry_flow.FlowManager(
|
|
|
|
hass, self._async_create_login_flow,
|
|
|
|
self._async_finish_login_flow)
|
2018-07-13 13:31:20 +00:00
|
|
|
self._access_tokens = OrderedDict()
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def active(self):
|
|
|
|
"""Return if any auth providers are registered."""
|
|
|
|
return bool(self._providers)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def support_legacy(self):
|
|
|
|
"""
|
|
|
|
Return if legacy_api_password auth providers are registered.
|
|
|
|
|
|
|
|
Should be removed when we removed legacy_api_password auth providers.
|
|
|
|
"""
|
|
|
|
for provider_type, _ in self._providers:
|
|
|
|
if provider_type == 'legacy_api_password':
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
2018-07-13 13:31:20 +00:00
|
|
|
def auth_providers(self):
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Return a list of available auth providers."""
|
2018-07-13 13:31:20 +00:00
|
|
|
return list(self._providers.values())
|
|
|
|
|
|
|
|
async def async_get_users(self):
|
|
|
|
"""Retrieve all users."""
|
|
|
|
return await self._store.async_get_users()
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
async def async_get_user(self, user_id):
|
|
|
|
"""Retrieve a user."""
|
|
|
|
return await self._store.async_get_user(user_id)
|
|
|
|
|
|
|
|
async def async_create_system_user(self, name):
|
|
|
|
"""Create a system user."""
|
|
|
|
return await self._store.async_create_user(
|
|
|
|
name=name,
|
|
|
|
system_generated=True,
|
|
|
|
is_active=True,
|
|
|
|
)
|
|
|
|
|
2018-07-13 13:31:20 +00:00
|
|
|
async def async_create_user(self, name):
|
|
|
|
"""Create a user."""
|
2018-07-15 18:46:15 +00:00
|
|
|
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)
|
2018-07-13 13:31:20 +00:00
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
async def async_get_or_create_user(self, credentials):
|
|
|
|
"""Get or create a user."""
|
|
|
|
if not credentials.is_new:
|
|
|
|
for user in await self._store.async_get_users():
|
|
|
|
for creds in user.credentials:
|
|
|
|
if creds.id == credentials.id:
|
|
|
|
return user
|
|
|
|
|
|
|
|
raise ValueError('Unable to find the user.')
|
|
|
|
|
|
|
|
auth_provider = self._async_get_auth_provider(credentials)
|
2018-07-13 13:31:20 +00:00
|
|
|
|
|
|
|
if auth_provider is None:
|
|
|
|
raise RuntimeError('Credential with unknown provider encountered')
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
info = await auth_provider.async_user_meta_for_credentials(
|
|
|
|
credentials)
|
|
|
|
|
2018-07-15 18:46:15 +00:00
|
|
|
return await self._store.async_create_user(
|
|
|
|
credentials=credentials,
|
|
|
|
name=info.get('name'),
|
2018-07-19 20:10:36 +00:00
|
|
|
is_active=info.get('is_active', False)
|
2018-07-15 18:46:15 +00:00
|
|
|
)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
|
|
|
async def async_link_user(self, user, credentials):
|
|
|
|
"""Link credentials to an existing user."""
|
|
|
|
await self._store.async_link_user(user, credentials)
|
|
|
|
|
|
|
|
async def async_remove_user(self, user):
|
|
|
|
"""Remove a user."""
|
2018-07-13 13:31:20 +00:00
|
|
|
tasks = [
|
|
|
|
self.async_remove_credentials(credentials)
|
|
|
|
for credentials in user.credentials
|
|
|
|
]
|
|
|
|
|
|
|
|
if tasks:
|
|
|
|
await asyncio.wait(tasks)
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
await self._store.async_remove_user(user)
|
|
|
|
|
2018-07-15 18:46:15 +00:00
|
|
|
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."""
|
2018-07-15 21:09:05 +00:00
|
|
|
if user.is_owner:
|
|
|
|
raise ValueError('Unable to deactive the owner')
|
2018-07-15 18:46:15 +00:00
|
|
|
await self._store.async_deactivate_user(user)
|
|
|
|
|
2018-07-13 13:31:20 +00:00
|
|
|
async def async_remove_credentials(self, credentials):
|
|
|
|
"""Remove credentials."""
|
|
|
|
provider = self._async_get_auth_provider(credentials)
|
|
|
|
|
|
|
|
if (provider is not None and
|
|
|
|
hasattr(provider, 'async_will_remove_credentials')):
|
|
|
|
await provider.async_will_remove_credentials(credentials)
|
|
|
|
|
|
|
|
await self._store.async_remove_credentials(credentials)
|
|
|
|
|
2018-07-13 09:43:08 +00:00
|
|
|
async def async_create_refresh_token(self, user, client_id=None):
|
|
|
|
"""Create a new refresh token for a user."""
|
|
|
|
if not user.is_active:
|
|
|
|
raise ValueError('User is not active')
|
|
|
|
|
|
|
|
if user.system_generated and client_id is not None:
|
|
|
|
raise ValueError(
|
|
|
|
'System generated users cannot have refresh tokens connected '
|
|
|
|
'to a client.')
|
|
|
|
|
|
|
|
if not user.system_generated and client_id is None:
|
|
|
|
raise ValueError('Client is required to generate a refresh token.')
|
|
|
|
|
|
|
|
return await self._store.async_create_refresh_token(user, client_id)
|
|
|
|
|
|
|
|
async def async_get_refresh_token(self, token):
|
|
|
|
"""Get refresh token by token."""
|
|
|
|
return await self._store.async_get_refresh_token(token)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_create_access_token(self, refresh_token):
|
|
|
|
"""Create a new access token."""
|
|
|
|
access_token = models.AccessToken(refresh_token=refresh_token)
|
|
|
|
self._access_tokens[access_token.token] = access_token
|
|
|
|
return access_token
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_get_access_token(self, token):
|
|
|
|
"""Get an access token."""
|
|
|
|
tkn = self._access_tokens.get(token)
|
|
|
|
|
|
|
|
if tkn is None:
|
2018-07-17 07:24:51 +00:00
|
|
|
_LOGGER.debug('Attempt to get non-existing access token')
|
2018-07-13 09:43:08 +00:00
|
|
|
return None
|
|
|
|
|
2018-07-15 18:46:15 +00:00
|
|
|
if tkn.expired or not tkn.refresh_token.user.is_active:
|
2018-07-17 07:24:51 +00:00
|
|
|
if tkn.expired:
|
|
|
|
_LOGGER.debug('Attempt to get expired access token')
|
|
|
|
else:
|
|
|
|
_LOGGER.debug('Attempt to get access token for inactive user')
|
2018-07-13 09:43:08 +00:00
|
|
|
self._access_tokens.pop(token)
|
|
|
|
return None
|
|
|
|
|
|
|
|
return tkn
|
|
|
|
|
2018-08-09 11:24:14 +00:00
|
|
|
async def _async_create_login_flow(self, handler, *, context, data):
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Create a login flow."""
|
|
|
|
auth_provider = self._providers[handler]
|
|
|
|
|
2018-08-13 09:27:18 +00:00
|
|
|
return await auth_provider.async_credential_flow(context)
|
2018-07-13 09:43:08 +00:00
|
|
|
|
2018-08-13 09:27:18 +00:00
|
|
|
async def _async_finish_login_flow(self, context, result):
|
2018-07-13 09:43:08 +00:00
|
|
|
"""Result of a credential login flow."""
|
|
|
|
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
|
|
|
return None
|
|
|
|
|
|
|
|
auth_provider = self._providers[result['handler']]
|
|
|
|
return await auth_provider.async_get_or_create_credentials(
|
|
|
|
result['data'])
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_get_auth_provider(self, credentials):
|
|
|
|
"""Helper to get auth provider from a set of credentials."""
|
|
|
|
auth_provider_key = (credentials.auth_provider_type,
|
|
|
|
credentials.auth_provider_id)
|
2018-07-13 13:31:20 +00:00
|
|
|
return self._providers.get(auth_provider_key)
|
2018-07-15 18:46:15 +00:00
|
|
|
|
|
|
|
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
|