core/homeassistant/auth/__init__.py

486 lines
16 KiB
Python
Raw Normal View History

2018-07-13 09:43:08 +00:00
"""Provide an authentication layer for Home Assistant."""
import asyncio
import logging
from collections import OrderedDict
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, cast
2018-07-13 09:43:08 +00:00
import jwt
2018-07-13 09:43:08 +00:00
from homeassistant import data_entry_flow
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
2018-07-13 09:43:08 +00:00
from . import auth_store, models
from .const import GROUP_ID_ADMIN
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
2018-07-13 09:43:08 +00:00
2019-07-31 19:25:30 +00:00
EVENT_USER_ADDED = "user_added"
EVENT_USER_REMOVED = "user_removed"
2018-10-11 15:06:51 +00:00
2018-07-13 09:43:08 +00:00
_LOGGER = logging.getLogger(__name__)
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
_ProviderKey = Tuple[str, Optional[str]]
_ProviderDict = Dict[_ProviderKey, AuthProvider]
2018-07-13 09:43:08 +00:00
async def auth_manager_from_config(
2019-07-31 19:25:30 +00:00
hass: HomeAssistant,
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]],
) -> "AuthManager":
"""Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
mfa modules exist in configs.
"""
2018-07-13 09:43:08 +00:00
store = auth_store.AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
2019-07-31 19:25:30 +00:00
*(
auth_provider_from_config(hass, store, config)
for config in provider_configs
)
)
2018-07-13 09:43:08 +00:00
else:
providers = ()
2018-07-13 09:43:08 +00:00
# So returned auth providers are in same order as config
provider_hash: _ProviderDict = OrderedDict()
2018-07-13 09:43:08 +00:00
for provider in providers:
key = (provider.type, provider.id)
provider_hash[key] = provider
if module_configs:
modules = await asyncio.gather(
2019-07-31 19:25:30 +00:00
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
)
else:
modules = ()
# So returned auth modules are in same order as config
module_hash: _MfaModuleDict = OrderedDict()
for module in modules:
module_hash[module.id] = module
manager = AuthManager(hass, store, provider_hash, module_hash)
2018-07-13 09:43:08 +00:00
return manager
class AuthManager:
"""Manage the authentication for Home Assistant."""
2019-07-31 19:25:30 +00:00
def __init__(
self,
hass: HomeAssistant,
store: auth_store.AuthStore,
providers: _ProviderDict,
mfa_modules: _MfaModuleDict,
) -> None:
2018-07-13 09:43:08 +00:00
"""Initialize the auth manager."""
self.hass = hass
2018-07-13 09:43:08 +00:00
self._store = store
self._providers = providers
self._mfa_modules = mfa_modules
2018-07-13 09:43:08 +00:00
self.login_flow = data_entry_flow.FlowManager(
2019-07-31 19:25:30 +00:00
hass, self._async_create_login_flow, self._async_finish_login_flow
)
2018-07-13 09:43:08 +00:00
@property
def auth_providers(self) -> List[AuthProvider]:
2018-07-13 09:43:08 +00:00
"""Return a list of available auth providers."""
return list(self._providers.values())
@property
def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
2019-07-31 19:25:30 +00:00
def get_auth_provider(
self, provider_type: str, provider_id: str
) -> Optional[AuthProvider]:
"""Return an auth provider, None if not found."""
return self._providers.get((provider_type, provider_id))
2019-07-31 19:25:30 +00:00
def get_auth_providers(self, provider_type: str) -> List[AuthProvider]:
"""Return a List of auth provider of one type, Empty if not found."""
2019-07-31 19:25:30 +00:00
return [
provider
for (p_type, _), provider in self._providers.items()
if p_type == provider_type
]
2019-07-31 19:25:30 +00:00
def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
"""Return a multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
async def async_get_users(self) -> List[models.User]:
"""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: str) -> Optional[models.User]:
2018-07-13 09:43:08 +00:00
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_get_owner(self) -> Optional[models.User]:
"""Retrieve the owner."""
users = await self.async_get_users()
return next((user for user in users if user.is_owner), None)
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
"""Retrieve all groups."""
return await self._store.async_get_group(group_id)
async def async_get_user_by_credentials(
2019-07-31 19:25:30 +00:00
self, credentials: models.Credentials
) -> Optional[models.User]:
"""Get a user by credential, return None if not found."""
for user in await self.async_get_users():
for creds in user.credentials:
if creds.id == credentials.id:
return user
return None
async def async_create_system_user(
2019-07-31 19:25:30 +00:00
self, name: str, group_ids: Optional[List[str]] = None
) -> models.User:
2018-07-13 09:43:08 +00:00
"""Create a system user."""
2018-10-11 15:06:51 +00:00
user = await self._store.async_create_user(
2019-07-31 19:25:30 +00:00
name=name, system_generated=True, is_active=True, group_ids=group_ids or []
2018-07-13 09:43:08 +00:00
)
2019-07-31 19:25:30 +00:00
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
2018-10-11 15:06:51 +00:00
return user
async def async_create_user(self, name: str) -> models.User:
"""Create a user."""
kwargs: Dict[str, Any] = {
2019-07-31 19:25:30 +00:00
"name": name,
"is_active": True,
"group_ids": [GROUP_ID_ADMIN],
}
if await self._user_should_be_owner():
2019-07-31 19:25:30 +00:00
kwargs["is_owner"] = True
2018-10-11 15:06:51 +00:00
user = await self._store.async_create_user(**kwargs)
2019-07-31 19:25:30 +00:00
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
2018-10-11 15:06:51 +00:00
return user
2019-07-31 19:25:30 +00:00
async def async_get_or_create_user(
self, credentials: models.Credentials
) -> models.User:
2018-07-13 09:43:08 +00:00
"""Get or create a user."""
if not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials)
if user is None:
2019-07-31 19:25:30 +00:00
raise ValueError("Unable to find the user.")
return user
2018-07-13 09:43:08 +00:00
auth_provider = self._async_get_auth_provider(credentials)
if auth_provider is None:
2019-07-31 19:25:30 +00:00
raise RuntimeError("Credential with unknown provider encountered")
2019-07-31 19:25:30 +00:00
info = await auth_provider.async_user_meta_for_credentials(credentials)
2018-07-13 09:43:08 +00:00
2018-10-11 15:06:51 +00:00
user = await self._store.async_create_user(
credentials=credentials,
name=info.name,
is_active=info.is_active,
group_ids=[GROUP_ID_ADMIN],
)
2018-07-13 09:43:08 +00:00
2019-07-31 19:25:30 +00:00
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
2018-10-11 15:06:51 +00:00
return user
2019-07-31 19:25:30 +00:00
async def async_link_user(
self, user: models.User, credentials: models.Credentials
) -> None:
2018-07-13 09:43:08 +00:00
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user: models.User) -> None:
2018-07-13 09:43:08 +00:00
"""Remove a user."""
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)
2019-07-31 19:25:30 +00:00
self.hass.bus.async_fire(EVENT_USER_REMOVED, {"user_id": user.id})
2018-10-11 15:06:51 +00:00
2019-07-31 19:25:30 +00:00
async def async_update_user(
self,
user: models.User,
name: Optional[str] = None,
group_ids: Optional[List[str]] = None,
) -> None:
"""Update a user."""
kwargs: Dict[str, Any] = {}
if name is not None:
2019-07-31 19:25:30 +00:00
kwargs["name"] = name
if group_ids is not None:
2019-07-31 19:25:30 +00:00
kwargs["group_ids"] = group_ids
await self._store.async_update_user(user, **kwargs)
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
await self._store.async_activate_user(user)
async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user."""
if user.is_owner:
2019-07-31 19:25:30 +00:00
raise ValueError("Unable to deactive the owner")
await self._store.async_deactivate_user(user)
2019-07-31 19:25:30 +00:00
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
provider = self._async_get_auth_provider(credentials)
2019-07-31 19:25:30 +00:00
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
# https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials( # type: ignore
2019-07-31 19:25:30 +00:00
credentials
)
await self._store.async_remove_credentials(credentials)
2019-07-31 19:25:30 +00:00
async def async_enable_user_mfa(
self, user: models.User, mfa_module_id: str, data: Any
) -> None:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
2019-07-31 19:25:30 +00:00
raise ValueError(
"System generated users cannot enable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
await module.async_setup_user(user.id, data)
2019-07-31 19:25:30 +00:00
async def async_disable_user_mfa(
self, user: models.User, mfa_module_id: str
) -> None:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
2019-07-31 19:25:30 +00:00
raise ValueError(
"System generated users cannot disable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
await module.async_depose_user(user.id)
async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
"""List enabled mfa modules for user."""
modules: Dict[str, str] = OrderedDict()
for module_id, module in self._mfa_modules.items():
if await module.async_is_user_setup(user.id):
modules[module_id] = module.name
return modules
async def async_create_refresh_token(
2019-07-31 19:25:30 +00:00
self,
user: models.User,
client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
2018-07-13 09:43:08 +00:00
"""Create a new refresh token for a user."""
if not user.is_active:
2019-07-31 19:25:30 +00:00
raise ValueError("User is not active")
2018-07-13 09:43:08 +00:00
if user.system_generated and client_id is not None:
raise ValueError(
2019-07-31 19:25:30 +00:00
"System generated users cannot have refresh tokens connected "
"to a client."
)
2018-07-13 09:43:08 +00:00
if token_type is None:
if user.system_generated:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
2019-07-31 19:25:30 +00:00
"System generated users can only have system type " "refresh tokens"
)
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
2019-07-31 19:25:30 +00:00
raise ValueError("Client is required to generate a refresh token.")
2018-07-13 09:43:08 +00:00
2019-07-31 19:25:30 +00:00
if (
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
and client_name is None
):
raise ValueError("Client_name is required for long-lived access " "token")
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values():
2019-07-31 19:25:30 +00:00
if (
token.client_name == client_name
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
):
# Each client_name can only have one
# long_lived_access_token type of refresh token
raise ValueError(f"{client_name} already exists")
return await self._store.async_create_refresh_token(
2019-07-31 19:25:30 +00:00
user,
client_id,
client_name,
client_icon,
token_type,
access_token_expiration,
)
2018-07-13 09:43:08 +00:00
async def async_get_refresh_token(
2019-07-31 19:25:30 +00:00
self, token_id: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(
2019-07-31 19:25:30 +00:00
self, token: str
) -> Optional[models.RefreshToken]:
2018-07-13 09:43:08 +00:00
"""Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token)
2018-07-13 09:43:08 +00:00
2019-07-31 19:25:30 +00:00
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken
) -> None:
"""Delete a refresh token."""
await self._store.async_remove_refresh_token(refresh_token)
2018-07-13 09:43:08 +00:00
@callback
2019-07-31 19:25:30 +00:00
def async_create_access_token(
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
) -> str:
2018-07-13 09:43:08 +00:00
"""Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
now = dt_util.utcnow()
2019-07-31 19:25:30 +00:00
return jwt.encode(
{
"iss": refresh_token.id,
"iat": now,
"exp": now + refresh_token.access_token_expiration,
},
refresh_token.jwt_key,
algorithm="HS256",
).decode()
async def async_validate_access_token(
2019-07-31 19:25:30 +00:00
self, token: str
) -> Optional[models.RefreshToken]:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
except jwt.InvalidTokenError:
return None
2018-07-13 09:43:08 +00:00
refresh_token = await self.async_get_refresh_token(
2019-07-31 19:25:30 +00:00
cast(str, unverif_claims.get("iss"))
)
if refresh_token is None:
2019-07-31 19:25:30 +00:00
jwt_key = ""
issuer = ""
else:
jwt_key = refresh_token.jwt_key
issuer = refresh_token.id
try:
2019-07-31 19:25:30 +00:00
jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
except jwt.InvalidTokenError:
2018-07-13 09:43:08 +00:00
return None
if refresh_token is None or not refresh_token.user.is_active:
2018-07-13 09:43:08 +00:00
return None
return refresh_token
2018-07-13 09:43:08 +00:00
async def _async_create_login_flow(
2019-07-31 19:25:30 +00:00
self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any]
) -> data_entry_flow.FlowHandler:
2018-07-13 09:43:08 +00:00
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_login_flow(context)
2018-07-13 09:43:08 +00:00
async def _async_finish_login_flow(
2019-07-31 19:25:30 +00:00
self, flow: LoginFlow, result: Dict[str, Any]
) -> Dict[str, Any]:
"""Return a user as result of login flow."""
2019-07-31 19:25:30 +00:00
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return result
2018-07-13 09:43:08 +00:00
# we got final result
2019-07-31 19:25:30 +00:00
if isinstance(result["data"], models.User):
result["result"] = result["data"]
return result
2019-07-31 19:25:30 +00:00
auth_provider = self._providers[result["handler"]]
credentials = await auth_provider.async_get_or_create_credentials(
2019-07-31 19:25:30 +00:00
result["data"]
)
2018-07-13 09:43:08 +00:00
if flow.context.get("credential_only"):
2019-07-31 19:25:30 +00:00
result["result"] = credentials
return result
# multi-factor module cannot enabled for new credential
# which has not linked to a user yet
if auth_provider.support_mfa and not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials)
if user is not None:
modules = await self.async_get_enabled_mfa(user)
if modules:
flow.user = user
flow.available_mfa_modules = modules
return await flow.async_step_select_mfa_module()
2019-07-31 19:25:30 +00:00
result["result"] = await self.async_get_or_create_user(credentials)
return result
2018-07-13 09:43:08 +00:00
@callback
def _async_get_auth_provider(
2019-07-31 19:25:30 +00:00
self, credentials: models.Credentials
) -> Optional[AuthProvider]:
"""Get auth provider from a set of credentials."""
2019-07-31 19:25:30 +00:00
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) -> bool:
"""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