Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a1e4b98fa8 | ||
![]() |
4f26935756 | ||
![]() |
b9f004b549 | ||
![]() |
48574cd98b | ||
![]() |
4f4a14bd62 |
14 changed files with 416 additions and 203 deletions
|
@ -211,7 +211,7 @@ class AuthManager:
|
||||||
async def async_enable_user_mfa(self, user: models.User,
|
async def async_enable_user_mfa(self, user: models.User,
|
||||||
mfa_module_id: str, data: Any) -> None:
|
mfa_module_id: str, data: Any) -> None:
|
||||||
"""Enable a multi-factor auth module for user."""
|
"""Enable a multi-factor auth module for user."""
|
||||||
if user.system_generated:
|
if user.group.system_generated:
|
||||||
raise ValueError('System generated users cannot enable '
|
raise ValueError('System generated users cannot enable '
|
||||||
'multi-factor auth module.')
|
'multi-factor auth module.')
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ class AuthManager:
|
||||||
async def async_disable_user_mfa(self, user: models.User,
|
async def async_disable_user_mfa(self, user: models.User,
|
||||||
mfa_module_id: str) -> None:
|
mfa_module_id: str) -> None:
|
||||||
"""Disable a multi-factor auth module for user."""
|
"""Disable a multi-factor auth module for user."""
|
||||||
if user.system_generated:
|
if user.group.system_generated:
|
||||||
raise ValueError('System generated users cannot disable '
|
raise ValueError('System generated users cannot disable '
|
||||||
'multi-factor auth module.')
|
'multi-factor auth module.')
|
||||||
|
|
||||||
|
@ -255,18 +255,19 @@ class AuthManager:
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise ValueError('User is not active')
|
raise ValueError('User is not active')
|
||||||
|
|
||||||
if user.system_generated and client_id is not None:
|
if user.group.system_generated and client_id is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'System generated users cannot have refresh tokens connected '
|
'System generated users cannot have refresh tokens connected '
|
||||||
'to a client.')
|
'to a client.')
|
||||||
|
|
||||||
if token_type is None:
|
if token_type is None:
|
||||||
if user.system_generated:
|
if user.group.system_generated:
|
||||||
token_type = models.TOKEN_TYPE_SYSTEM
|
token_type = models.TOKEN_TYPE_SYSTEM
|
||||||
else:
|
else:
|
||||||
token_type = models.TOKEN_TYPE_NORMAL
|
token_type = models.TOKEN_TYPE_NORMAL
|
||||||
|
|
||||||
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
if user.group.system_generated != (token_type ==
|
||||||
|
models.TOKEN_TYPE_SYSTEM):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'System generated users can only have system type '
|
'System generated users can only have system type '
|
||||||
'refresh tokens')
|
'refresh tokens')
|
||||||
|
@ -414,7 +415,7 @@ class AuthManager:
|
||||||
being created.
|
being created.
|
||||||
"""
|
"""
|
||||||
for user in await self._store.async_get_users():
|
for user in await self._store.async_get_users():
|
||||||
if not user.system_generated:
|
if not user.group.system_generated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,20 +1,85 @@
|
||||||
"""Storage for auth models."""
|
"""Storage for auth models."""
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import hmac
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Any, Dict, List, Optional # noqa: F401
|
from typing import Any, Dict, List, Optional # noqa: F401
|
||||||
import hmac
|
import uuid
|
||||||
|
|
||||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import storage
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 2
|
||||||
STORAGE_KEY = 'auth'
|
STORAGE_KEY = 'auth'
|
||||||
|
|
||||||
|
|
||||||
|
class DataStore(storage.Store):
|
||||||
|
"""Store the auth data on disk using JSON."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the data store."""
|
||||||
|
super().__init__(hass, STORAGE_VERSION, STORAGE_KEY, True)
|
||||||
|
|
||||||
|
async def _async_migrate_func(self, old_version: int,
|
||||||
|
old_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Migrate to the new version."""
|
||||||
|
if old_version <= 1:
|
||||||
|
system_group_id = old_data['system_user_group_id'] = \
|
||||||
|
uuid.uuid4().hex
|
||||||
|
family_group_id = old_data['default_new_user_group_id'] = \
|
||||||
|
uuid.uuid4().hex
|
||||||
|
|
||||||
|
old_data['groups'] = [
|
||||||
|
{
|
||||||
|
'name': 'System',
|
||||||
|
'id': system_group_id,
|
||||||
|
'system_generated': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Family',
|
||||||
|
'id': family_group_id,
|
||||||
|
'system_generated': False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for user_dict in old_data['users']:
|
||||||
|
if user_dict.pop('system_generated'):
|
||||||
|
group_id = system_group_id
|
||||||
|
else:
|
||||||
|
group_id = family_group_id
|
||||||
|
|
||||||
|
user_dict['group_id'] = group_id
|
||||||
|
|
||||||
|
refresh_tokens = []
|
||||||
|
|
||||||
|
for rt_dict in old_data['refresh_tokens']:
|
||||||
|
if 'jwt_key' not in rt_dict:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if 'token_type' not in rt_dict:
|
||||||
|
if rt_dict['client_id'] is None:
|
||||||
|
token_type = models.TOKEN_TYPE_SYSTEM
|
||||||
|
else:
|
||||||
|
token_type = models.TOKEN_TYPE_NORMAL
|
||||||
|
|
||||||
|
rt_dict['token_type'] = token_type
|
||||||
|
|
||||||
|
rt_dict.setdefault('last_used_at', None)
|
||||||
|
rt_dict.setdefault('client_name', None)
|
||||||
|
rt_dict.setdefault('client_icon', None)
|
||||||
|
rt_dict.setdefault('last_used_ip', None)
|
||||||
|
|
||||||
|
refresh_tokens.append(rt_dict)
|
||||||
|
|
||||||
|
old_data['refresh_tokens'] = refresh_tokens
|
||||||
|
|
||||||
|
return old_data
|
||||||
|
|
||||||
|
|
||||||
class AuthStore:
|
class AuthStore:
|
||||||
"""Stores authentication info.
|
"""Stores authentication info.
|
||||||
|
|
||||||
|
@ -28,8 +93,10 @@ class AuthStore:
|
||||||
"""Initialize the auth store."""
|
"""Initialize the auth store."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._users = None # type: Optional[Dict[str, models.User]]
|
self._users = None # type: Optional[Dict[str, models.User]]
|
||||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
self._groups = None # type: Optional[Dict[str, models.Group]]
|
||||||
private=True)
|
self._default_new_user_group_id = None # type: Optional[str]
|
||||||
|
self._system_user_group_id = None # type: Optional[str]
|
||||||
|
self._store = DataStore(hass)
|
||||||
|
|
||||||
async def async_get_users(self) -> List[models.User]:
|
async def async_get_users(self) -> List[models.User]:
|
||||||
"""Retrieve all users."""
|
"""Retrieve all users."""
|
||||||
|
@ -50,15 +117,26 @@ class AuthStore:
|
||||||
async def async_create_user(
|
async def async_create_user(
|
||||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||||
is_active: Optional[bool] = None,
|
is_active: Optional[bool] = None,
|
||||||
|
group_id: Optional[str] = None,
|
||||||
system_generated: Optional[bool] = None,
|
system_generated: Optional[bool] = None,
|
||||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
credentials: Optional[models.Credentials] = None) -> models.User:
|
||||||
"""Create a new user."""
|
"""Create a new user."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
assert self._users is not None
|
|
||||||
|
assert self._users is not None
|
||||||
|
assert self._groups is not None
|
||||||
|
assert self._system_user_group_id is not None
|
||||||
|
assert self._default_new_user_group_id is not None
|
||||||
|
|
||||||
|
if system_generated:
|
||||||
|
group_id = self._system_user_group_id
|
||||||
|
elif group_id is None:
|
||||||
|
group_id = self._default_new_user_group_id
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'name': name
|
'name': name,
|
||||||
|
'group': self._groups[group_id],
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
if is_owner is not None:
|
if is_owner is not None:
|
||||||
|
@ -67,9 +145,6 @@ class AuthStore:
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
kwargs['is_active'] = is_active
|
kwargs['is_active'] = is_active
|
||||||
|
|
||||||
if system_generated is not None:
|
|
||||||
kwargs['system_generated'] = system_generated
|
|
||||||
|
|
||||||
new_user = models.User(**kwargs)
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
self._users[new_user.id] = new_user
|
self._users[new_user.id] = new_user
|
||||||
|
@ -214,14 +289,32 @@ class AuthStore:
|
||||||
if self._users is not None:
|
if self._users is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
users = OrderedDict() # type: Dict[str, models.User]
|
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
self._users = users
|
self._set_defaults()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
users = OrderedDict() # type: Dict[str, models.User]
|
||||||
|
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||||
|
|
||||||
|
# When creating objects we mention each attribute explicetely. This
|
||||||
|
# prevents crashing if user rolls back HA version after a new property
|
||||||
|
# was added.
|
||||||
|
|
||||||
|
for group_dict in data['groups']:
|
||||||
|
groups[group_dict['id']] = models.Group(
|
||||||
|
name=group_dict['name'],
|
||||||
|
id=group_dict['id'],
|
||||||
|
system_generated=group_dict['system_generated'],
|
||||||
|
)
|
||||||
|
|
||||||
for user_dict in data['users']:
|
for user_dict in data['users']:
|
||||||
users[user_dict['id']] = models.User(**user_dict)
|
users[user_dict['id']] = models.User(
|
||||||
|
name=user_dict['name'],
|
||||||
|
group=groups[user_dict['group_id']],
|
||||||
|
id=user_dict['id'],
|
||||||
|
is_owner=user_dict['is_owner'],
|
||||||
|
is_active=user_dict['is_active'],
|
||||||
|
)
|
||||||
|
|
||||||
for cred_dict in data['credentials']:
|
for cred_dict in data['credentials']:
|
||||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||||
|
@ -233,10 +326,6 @@ class AuthStore:
|
||||||
))
|
))
|
||||||
|
|
||||||
for rt_dict in data['refresh_tokens']:
|
for rt_dict in data['refresh_tokens']:
|
||||||
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
|
||||||
if 'jwt_key' not in rt_dict:
|
|
||||||
continue
|
|
||||||
|
|
||||||
created_at = dt_util.parse_datetime(rt_dict['created_at'])
|
created_at = dt_util.parse_datetime(rt_dict['created_at'])
|
||||||
if created_at is None:
|
if created_at is None:
|
||||||
getLogger(__name__).error(
|
getLogger(__name__).error(
|
||||||
|
@ -244,15 +333,7 @@ class AuthStore:
|
||||||
'%(created_at)s for user_id %(user_id)s', rt_dict)
|
'%(created_at)s for user_id %(user_id)s', rt_dict)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
token_type = rt_dict.get('token_type')
|
last_used_at_str = rt_dict['last_used_at']
|
||||||
if token_type is None:
|
|
||||||
if rt_dict['client_id'] is None:
|
|
||||||
token_type = models.TOKEN_TYPE_SYSTEM
|
|
||||||
else:
|
|
||||||
token_type = models.TOKEN_TYPE_NORMAL
|
|
||||||
|
|
||||||
# old refresh_token don't have last_used_at (pre-0.78)
|
|
||||||
last_used_at_str = rt_dict.get('last_used_at')
|
|
||||||
if last_used_at_str:
|
if last_used_at_str:
|
||||||
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
||||||
else:
|
else:
|
||||||
|
@ -262,21 +343,23 @@ class AuthStore:
|
||||||
id=rt_dict['id'],
|
id=rt_dict['id'],
|
||||||
user=users[rt_dict['user_id']],
|
user=users[rt_dict['user_id']],
|
||||||
client_id=rt_dict['client_id'],
|
client_id=rt_dict['client_id'],
|
||||||
# use dict.get to keep backward compatibility
|
client_name=rt_dict['client_name'],
|
||||||
client_name=rt_dict.get('client_name'),
|
client_icon=rt_dict['client_icon'],
|
||||||
client_icon=rt_dict.get('client_icon'),
|
token_type=rt_dict['token_type'],
|
||||||
token_type=token_type,
|
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
access_token_expiration=timedelta(
|
access_token_expiration=timedelta(
|
||||||
seconds=rt_dict['access_token_expiration']),
|
seconds=rt_dict['access_token_expiration']),
|
||||||
token=rt_dict['token'],
|
token=rt_dict['token'],
|
||||||
jwt_key=rt_dict['jwt_key'],
|
jwt_key=rt_dict['jwt_key'],
|
||||||
last_used_at=last_used_at,
|
last_used_at=last_used_at,
|
||||||
last_used_ip=rt_dict.get('last_used_ip'),
|
last_used_ip=rt_dict['last_used_ip'],
|
||||||
)
|
)
|
||||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||||
|
|
||||||
|
self._groups = groups
|
||||||
self._users = users
|
self._users = users
|
||||||
|
self._default_new_user_group_id = data['default_new_user_group_id']
|
||||||
|
self._system_user_group_id = data['system_user_group_id']
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_schedule_save(self) -> None:
|
def _async_schedule_save(self) -> None:
|
||||||
|
@ -290,18 +373,28 @@ class AuthStore:
|
||||||
def _data_to_save(self) -> Dict:
|
def _data_to_save(self) -> Dict:
|
||||||
"""Return the data to store."""
|
"""Return the data to store."""
|
||||||
assert self._users is not None
|
assert self._users is not None
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
{
|
{
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
|
'group_id': user.group.id,
|
||||||
'is_owner': user.is_owner,
|
'is_owner': user.is_owner,
|
||||||
'is_active': user.is_active,
|
'is_active': user.is_active,
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'system_generated': user.system_generated,
|
|
||||||
}
|
}
|
||||||
for user in self._users.values()
|
for user in self._users.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
groups = [
|
||||||
|
{
|
||||||
|
'name': group.name,
|
||||||
|
'id': group.id,
|
||||||
|
'system_generated': group.system_generated,
|
||||||
|
}
|
||||||
|
for group in self._groups.values()
|
||||||
|
]
|
||||||
|
|
||||||
credentials = [
|
credentials = [
|
||||||
{
|
{
|
||||||
'id': credential.id,
|
'id': credential.id,
|
||||||
|
@ -338,6 +431,26 @@ class AuthStore:
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'users': users,
|
'users': users,
|
||||||
|
'groups': groups,
|
||||||
'credentials': credentials,
|
'credentials': credentials,
|
||||||
'refresh_tokens': refresh_tokens,
|
'refresh_tokens': refresh_tokens,
|
||||||
|
'default_new_user_group_id': self._default_new_user_group_id,
|
||||||
|
'system_user_group_id': self._system_user_group_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _set_defaults(self) -> None:
|
||||||
|
"""Set default values for auth store."""
|
||||||
|
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||||
|
|
||||||
|
# Add default groups
|
||||||
|
system_group = models.Group(name='System', system_generated=True)
|
||||||
|
family_group = models.Group(name='Family')
|
||||||
|
|
||||||
|
self._default_new_user_group_id = family_group.id
|
||||||
|
self._system_user_group_id = system_group.id
|
||||||
|
|
||||||
|
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||||
|
groups[system_group.id] = system_group
|
||||||
|
groups[family_group.id] = family_group
|
||||||
|
|
||||||
|
self._groups = groups
|
||||||
|
|
|
@ -14,15 +14,25 @@ TOKEN_TYPE_SYSTEM = 'system'
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class Group:
|
||||||
|
"""A group."""
|
||||||
|
|
||||||
|
name = attr.ib(type=str) # type: Optional[str]
|
||||||
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
# System generated groups cannot be changed
|
||||||
|
system_generated = attr.ib(type=bool, default=False)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class User:
|
class User:
|
||||||
"""A user."""
|
"""A user."""
|
||||||
|
|
||||||
name = attr.ib(type=str) # type: Optional[str]
|
name = attr.ib(type=str) # type: Optional[str]
|
||||||
|
group = attr.ib(type=Group)
|
||||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
is_owner = attr.ib(type=bool, default=False)
|
is_owner = attr.ib(type=bool, default=False)
|
||||||
is_active = attr.ib(type=bool, default=False)
|
is_active = attr.ib(type=bool, default=False)
|
||||||
system_generated = attr.ib(type=bool, default=False)
|
|
||||||
|
|
||||||
# List of credentials of a user.
|
# List of credentials of a user.
|
||||||
credentials = attr.ib(
|
credentials = attr.ib(
|
||||||
|
|
|
@ -44,9 +44,9 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
assert context is not None
|
assert context is not None
|
||||||
users = await self.store.async_get_users()
|
users = await self.store.async_get_users()
|
||||||
available_users = {user.id: user.name
|
available_users = {
|
||||||
for user in users
|
user.id: user.name for user in users
|
||||||
if not user.system_generated and user.is_active}
|
if not user.group.system_generated and user.is_active}
|
||||||
|
|
||||||
return TrustedNetworksLoginFlow(
|
return TrustedNetworksLoginFlow(
|
||||||
self, cast(str, context.get('ip_address')), available_users)
|
self, cast(str, context.get('ip_address')), available_users)
|
||||||
|
@ -58,7 +58,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
|
|
||||||
users = await self.store.async_get_users()
|
users = await self.store.async_get_users()
|
||||||
for user in users:
|
for user in users:
|
||||||
if (not user.system_generated and
|
if (not user.group.system_generated and
|
||||||
user.is_active and
|
user.is_active and
|
||||||
user.id == user_id):
|
user.id == user_id):
|
||||||
for credential in await self.async_credentials():
|
for credential in await self.async_credentials():
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Offer API to configure Home Assistant auth."""
|
"""Offer API to configure Home Assistant auth."""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,61 +39,49 @@ async def async_setup(hass):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
@websocket_api.require_owner
|
@websocket_api.require_owner
|
||||||
def websocket_list(hass, connection, msg):
|
@websocket_api.async_response
|
||||||
|
async def websocket_list(hass, connection, msg):
|
||||||
"""Return a list of users."""
|
"""Return a list of users."""
|
||||||
async def send_users():
|
result = [_user_info(u) for u in await hass.auth.async_get_users()]
|
||||||
"""Send users."""
|
|
||||||
result = [_user_info(u) for u in await hass.auth.async_get_users()]
|
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(msg['id'], result))
|
websocket_api.result_message(msg['id'], result))
|
||||||
|
|
||||||
hass.async_add_job(send_users())
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
@websocket_api.require_owner
|
@websocket_api.require_owner
|
||||||
def websocket_delete(hass, connection, msg):
|
@websocket_api.async_response
|
||||||
|
async def websocket_delete(hass, connection, msg):
|
||||||
"""Delete a user."""
|
"""Delete a user."""
|
||||||
async def delete_user():
|
if msg['user_id'] == connection.request.get('hass_user').id:
|
||||||
"""Delete user."""
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
if msg['user_id'] == connection.request.get('hass_user').id:
|
msg['id'], 'no_delete_self',
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
'Unable to delete your own account'))
|
||||||
msg['id'], 'no_delete_self',
|
return
|
||||||
'Unable to delete your own account'))
|
|
||||||
return
|
|
||||||
|
|
||||||
user = await hass.auth.async_get_user(msg['user_id'])
|
user = await hass.auth.async_get_user(msg['user_id'])
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
msg['id'], 'not_found', 'User not found'))
|
msg['id'], 'not_found', 'User not found'))
|
||||||
return
|
return
|
||||||
|
|
||||||
await hass.auth.async_remove_user(user)
|
await hass.auth.async_remove_user(user)
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(msg['id']))
|
websocket_api.result_message(msg['id']))
|
||||||
|
|
||||||
hass.async_add_job(delete_user())
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
@websocket_api.require_owner
|
@websocket_api.require_owner
|
||||||
def websocket_create(hass, connection, msg):
|
@websocket_api.async_response
|
||||||
|
async def websocket_create(hass, connection, msg):
|
||||||
"""Create a user."""
|
"""Create a user."""
|
||||||
async def create_user():
|
user = await hass.auth.async_create_user(msg['name'])
|
||||||
"""Create a user."""
|
|
||||||
user = await hass.auth.async_create_user(msg['name'])
|
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(msg['id'], {
|
websocket_api.result_message(msg['id'], {
|
||||||
'user': _user_info(user)
|
'user': _user_info(user)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
hass.async_add_job(create_user())
|
|
||||||
|
|
||||||
|
|
||||||
def _user_info(user):
|
def _user_info(user):
|
||||||
|
@ -104,7 +91,9 @@ def _user_info(user):
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'is_owner': user.is_owner,
|
'is_owner': user.is_owner,
|
||||||
'is_active': user.is_active,
|
'is_active': user.is_active,
|
||||||
'system_generated': user.system_generated,
|
# Temp, backwards compat since 0.80, remove in 85
|
||||||
|
'system_generated': user.group.system_generated,
|
||||||
|
'group_id': user.group.id,
|
||||||
'credentials': [
|
'credentials': [
|
||||||
{
|
{
|
||||||
'type': c.auth_provider_type,
|
'type': c.auth_provider_type,
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.auth.providers import homeassistant as auth_ha
|
from homeassistant.auth.providers import homeassistant as auth_ha
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,121 +53,109 @@ def _get_provider(hass):
|
||||||
raise RuntimeError('Provider not found')
|
raise RuntimeError('Provider not found')
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
@websocket_api.require_owner
|
@websocket_api.require_owner
|
||||||
def websocket_create(hass, connection, msg):
|
@websocket_api.async_response
|
||||||
|
async def websocket_create(hass, connection, msg):
|
||||||
"""Create credentials and attach to a user."""
|
"""Create credentials and attach to a user."""
|
||||||
async def create_creds():
|
provider = _get_provider(hass)
|
||||||
"""Create credentials."""
|
await provider.async_initialize()
|
||||||
provider = _get_provider(hass)
|
|
||||||
await provider.async_initialize()
|
|
||||||
|
|
||||||
user = await hass.auth.async_get_user(msg['user_id'])
|
user = await hass.auth.async_get_user(msg['user_id'])
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
msg['id'], 'not_found', 'User not found'))
|
msg['id'], 'not_found', 'User not found'))
|
||||||
return
|
return
|
||||||
|
|
||||||
if user.system_generated:
|
if user.group.system_generated:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
msg['id'], 'system_generated',
|
msg['id'], 'system_generated',
|
||||||
'Cannot add credentials to a system generated user.'))
|
'Cannot add credentials to a system generated user.'))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await hass.async_add_executor_job(
|
await hass.async_add_executor_job(
|
||||||
provider.data.add_auth, msg['username'], msg['password'])
|
provider.data.add_auth, msg['username'], msg['password'])
|
||||||
except auth_ha.InvalidUser:
|
except auth_ha.InvalidUser:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
msg['id'], 'username_exists', 'Username already exists'))
|
msg['id'], 'username_exists', 'Username already exists'))
|
||||||
return
|
return
|
||||||
|
|
||||||
credentials = await provider.async_get_or_create_credentials({
|
credentials = await provider.async_get_or_create_credentials({
|
||||||
'username': msg['username']
|
'username': msg['username']
|
||||||
})
|
})
|
||||||
await hass.auth.async_link_user(user, credentials)
|
await hass.auth.async_link_user(user, credentials)
|
||||||
|
|
||||||
await provider.data.async_save()
|
await provider.data.async_save()
|
||||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id']))
|
connection.to_write.put_nowait(websocket_api.result_message(msg['id']))
|
||||||
|
|
||||||
hass.async_add_job(create_creds())
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
@websocket_api.require_owner
|
@websocket_api.require_owner
|
||||||
def websocket_delete(hass, connection, msg):
|
@websocket_api.async_response
|
||||||
|
async def websocket_delete(hass, connection, msg):
|
||||||
"""Delete username and related credential."""
|
"""Delete username and related credential."""
|
||||||
async def delete_creds():
|
provider = _get_provider(hass)
|
||||||
"""Delete user credentials."""
|
await provider.async_initialize()
|
||||||
provider = _get_provider(hass)
|
|
||||||
await provider.async_initialize()
|
|
||||||
|
|
||||||
credentials = await provider.async_get_or_create_credentials({
|
credentials = await provider.async_get_or_create_credentials({
|
||||||
'username': msg['username']
|
'username': msg['username']
|
||||||
})
|
})
|
||||||
|
|
||||||
# if not new, an existing credential exists.
|
# if not new, an existing credential exists.
|
||||||
# Removing the credential will also remove the auth.
|
# Removing the credential will also remove the auth.
|
||||||
if not credentials.is_new:
|
if not credentials.is_new:
|
||||||
await hass.auth.async_remove_credentials(credentials)
|
await hass.auth.async_remove_credentials(credentials)
|
||||||
|
|
||||||
connection.to_write.put_nowait(
|
|
||||||
websocket_api.result_message(msg['id']))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
provider.data.async_remove_auth(msg['username'])
|
|
||||||
await provider.data.async_save()
|
|
||||||
except auth_ha.InvalidUser:
|
|
||||||
connection.to_write.put_nowait(websocket_api.error_message(
|
|
||||||
msg['id'], 'auth_not_found', 'Given username was not found.'))
|
|
||||||
return
|
|
||||||
|
|
||||||
connection.to_write.put_nowait(
|
connection.to_write.put_nowait(
|
||||||
websocket_api.result_message(msg['id']))
|
websocket_api.result_message(msg['id']))
|
||||||
|
return
|
||||||
|
|
||||||
hass.async_add_job(delete_creds())
|
try:
|
||||||
|
provider.data.async_remove_auth(msg['username'])
|
||||||
|
|
||||||
@callback
|
|
||||||
def websocket_change_password(hass, connection, msg):
|
|
||||||
"""Change user password."""
|
|
||||||
async def change_password():
|
|
||||||
"""Change user password."""
|
|
||||||
user = connection.request.get('hass_user')
|
|
||||||
if user is None:
|
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
|
||||||
msg['id'], 'user_not_found', 'User not found'))
|
|
||||||
return
|
|
||||||
|
|
||||||
provider = _get_provider(hass)
|
|
||||||
await provider.async_initialize()
|
|
||||||
|
|
||||||
username = None
|
|
||||||
for credential in user.credentials:
|
|
||||||
if credential.auth_provider_type == provider.type:
|
|
||||||
username = credential.data['username']
|
|
||||||
break
|
|
||||||
|
|
||||||
if username is None:
|
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
|
||||||
msg['id'], 'credentials_not_found', 'Credentials not found'))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await provider.async_validate_login(
|
|
||||||
username, msg['current_password'])
|
|
||||||
except auth_ha.InvalidAuth:
|
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
|
||||||
msg['id'], 'invalid_password', 'Invalid password'))
|
|
||||||
return
|
|
||||||
|
|
||||||
await hass.async_add_executor_job(
|
|
||||||
provider.data.change_password, username, msg['new_password'])
|
|
||||||
await provider.data.async_save()
|
await provider.data.async_save()
|
||||||
|
except auth_ha.InvalidUser:
|
||||||
|
connection.to_write.put_nowait(websocket_api.error_message(
|
||||||
|
msg['id'], 'auth_not_found', 'Given username was not found.'))
|
||||||
|
return
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.to_write.put_nowait(
|
||||||
websocket_api.result_message(msg['id']))
|
websocket_api.result_message(msg['id']))
|
||||||
|
|
||||||
hass.async_add_job(change_password())
|
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def websocket_change_password(hass, connection, msg):
|
||||||
|
"""Change user password."""
|
||||||
|
user = connection.request.get('hass_user')
|
||||||
|
if user is None:
|
||||||
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
|
msg['id'], 'user_not_found', 'User not found'))
|
||||||
|
return
|
||||||
|
|
||||||
|
provider = _get_provider(hass)
|
||||||
|
await provider.async_initialize()
|
||||||
|
|
||||||
|
username = None
|
||||||
|
for credential in user.credentials:
|
||||||
|
if credential.auth_provider_type == provider.type:
|
||||||
|
username = credential.data['username']
|
||||||
|
break
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
|
msg['id'], 'credentials_not_found', 'Credentials not found'))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await provider.async_validate_login(
|
||||||
|
username, msg['current_password'])
|
||||||
|
except auth_ha.InvalidAuth:
|
||||||
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
|
msg['id'], 'invalid_password', 'Invalid password'))
|
||||||
|
return
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(
|
||||||
|
provider.data.change_password, username, msg['new_password'])
|
||||||
|
await provider.data.async_save()
|
||||||
|
|
||||||
|
connection.send_message_outside(
|
||||||
|
websocket_api.result_message(msg['id']))
|
||||||
|
|
|
@ -480,22 +480,24 @@ class ActiveConnection:
|
||||||
return wsock
|
return wsock
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_async_response(func, hass, connection, msg):
|
||||||
|
"""Create a response and handle exception."""
|
||||||
|
try:
|
||||||
|
await func(hass, connection, msg)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
connection.send_message_outside(error_message(
|
||||||
|
msg['id'], 'unknown', 'Unexpected error occurred'))
|
||||||
|
|
||||||
|
|
||||||
def async_response(func):
|
def async_response(func):
|
||||||
"""Decorate an async function to handle WebSocket API messages."""
|
"""Decorate an async function to handle WebSocket API messages."""
|
||||||
async def handle_msg_response(hass, connection, msg):
|
|
||||||
"""Create a response and handle exception."""
|
|
||||||
try:
|
|
||||||
await func(hass, connection, msg)
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
connection.send_message_outside(error_message(
|
|
||||||
msg['id'], 'unknown', 'Unexpected error occurred'))
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def schedule_handler(hass, connection, msg):
|
def schedule_handler(hass, connection, msg):
|
||||||
"""Schedule the handler."""
|
"""Schedule the handler."""
|
||||||
hass.async_create_task(handle_msg_response(hass, connection, msg))
|
hass.async_create_task(
|
||||||
|
_handle_async_response(func, hass, connection, msg))
|
||||||
|
|
||||||
return schedule_handler
|
return schedule_handler
|
||||||
|
|
||||||
|
@ -619,13 +621,13 @@ def ws_require_user(
|
||||||
return
|
return
|
||||||
|
|
||||||
if (only_system_user and
|
if (only_system_user and
|
||||||
not connection.user.system_generated):
|
not connection.user.group.system_generated):
|
||||||
output_error('only_system_user',
|
output_error('only_system_user',
|
||||||
'Only allowed as system user')
|
'Only allowed as system user')
|
||||||
return
|
return
|
||||||
|
|
||||||
if (not allow_system_user
|
if (not allow_system_user
|
||||||
and connection.user.system_generated):
|
and connection.user.group.system_generated):
|
||||||
output_error('not_system_user', 'Not allowed as system user')
|
output_error('not_system_user', 'Not allowed as system user')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Optional, Callable
|
from typing import Dict, Optional, Callable, Any
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
@ -63,7 +63,7 @@ class Store:
|
||||||
"""Return the config path."""
|
"""Return the config path."""
|
||||||
return self.hass.config.path(STORAGE_DIR, self.key)
|
return self.hass.config.path(STORAGE_DIR, self.key)
|
||||||
|
|
||||||
async def async_load(self):
|
async def async_load(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Load data.
|
"""Load data.
|
||||||
|
|
||||||
If the expected version does not match the given version, the migrate
|
If the expected version does not match the given version, the migrate
|
||||||
|
|
47
tests/auth/test_auth_store.py
Normal file
47
tests/auth/test_auth_store.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""Test the auth store."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from homeassistant.auth import auth_store, models
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_v1_v2(hass, hass_storage):
|
||||||
|
"""Test migrating auth store to add groups."""
|
||||||
|
hass_storage[auth_store.STORAGE_KEY] = json.loads(
|
||||||
|
load_fixture('auth_v1.json'))
|
||||||
|
store = auth_store.DataStore(hass)
|
||||||
|
data = await store.async_load()
|
||||||
|
|
||||||
|
assert len(data['groups']) == 2
|
||||||
|
system_group, family_group = data['groups']
|
||||||
|
|
||||||
|
assert system_group['system_generated'] is True
|
||||||
|
assert system_group['name'] == 'System'
|
||||||
|
|
||||||
|
assert family_group['system_generated'] is False
|
||||||
|
assert family_group['name'] == 'Family'
|
||||||
|
|
||||||
|
assert len(data['users']) == 2
|
||||||
|
owner, hassio = data['users']
|
||||||
|
|
||||||
|
assert owner['is_owner'] is True
|
||||||
|
assert owner['group_id'] == family_group['id']
|
||||||
|
|
||||||
|
assert hassio['is_owner'] is False
|
||||||
|
assert hassio['group_id'] == system_group['id']
|
||||||
|
|
||||||
|
assert len(data['refresh_tokens']) == 2
|
||||||
|
user_token, system_token = data['refresh_tokens']
|
||||||
|
|
||||||
|
assert user_token['token_type'] == models.TOKEN_TYPE_NORMAL
|
||||||
|
assert user_token['last_used_at'] is None
|
||||||
|
assert user_token['client_name'] is None
|
||||||
|
assert user_token['client_icon'] is None
|
||||||
|
assert user_token['last_used_ip'] is None
|
||||||
|
|
||||||
|
assert system_token['token_type'] == models.TOKEN_TYPE_SYSTEM
|
||||||
|
assert system_token['last_used_at'] is None
|
||||||
|
assert system_token['client_name'] is None
|
||||||
|
assert system_token['client_icon'] is None
|
||||||
|
assert system_token['last_used_ip'] is None
|
|
@ -334,7 +334,7 @@ async def test_generating_system_user(hass):
|
||||||
manager = await auth.auth_manager_from_config(hass, [], [])
|
manager = await auth.auth_manager_from_config(hass, [], [])
|
||||||
user = await manager.async_create_system_user('Hass.io')
|
user = await manager.async_create_system_user('Hass.io')
|
||||||
token = await manager.async_create_refresh_token(user)
|
token = await manager.async_create_refresh_token(user)
|
||||||
assert user.system_generated
|
assert user.group.system_generated
|
||||||
assert token is not None
|
assert token is not None
|
||||||
assert token.client_id is None
|
assert token.client_id is None
|
||||||
|
|
||||||
|
@ -343,7 +343,7 @@ async def test_refresh_token_requires_client_for_user(hass):
|
||||||
"""Test create refresh token for a user with client_id."""
|
"""Test create refresh token for a user with client_id."""
|
||||||
manager = await auth.auth_manager_from_config(hass, [], [])
|
manager = await auth.auth_manager_from_config(hass, [], [])
|
||||||
user = MockUser().add_to_auth_manager(manager)
|
user = MockUser().add_to_auth_manager(manager)
|
||||||
assert user.system_generated is False
|
assert user.group.system_generated is False
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await manager.async_create_refresh_token(user)
|
await manager.async_create_refresh_token(user)
|
||||||
|
@ -361,7 +361,7 @@ async def test_refresh_token_not_requires_client_for_system_user(hass):
|
||||||
"""Test create refresh token for a system user w/o client_id."""
|
"""Test create refresh token for a system user w/o client_id."""
|
||||||
manager = await auth.auth_manager_from_config(hass, [], [])
|
manager = await auth.auth_manager_from_config(hass, [], [])
|
||||||
user = await manager.async_create_system_user('Hass.io')
|
user = await manager.async_create_system_user('Hass.io')
|
||||||
assert user.system_generated is True
|
assert user.group.system_generated is True
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await manager.async_create_refresh_token(user, CLIENT_ID)
|
await manager.async_create_refresh_token(user, CLIENT_ID)
|
||||||
|
|
|
@ -355,11 +355,13 @@ class MockUser(auth_models.User):
|
||||||
'is_owner': is_owner,
|
'is_owner': is_owner,
|
||||||
'is_active': is_active,
|
'is_active': is_active,
|
||||||
'name': name,
|
'name': name,
|
||||||
'system_generated': system_generated,
|
# Filled in when added to hass/auth manager
|
||||||
|
'group': None,
|
||||||
}
|
}
|
||||||
if id is not None:
|
if id is not None:
|
||||||
kwargs['id'] = id
|
kwargs['id'] = id
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self._mock_system_generated = system_generated
|
||||||
|
|
||||||
def add_to_hass(self, hass):
|
def add_to_hass(self, hass):
|
||||||
"""Test helper to add entry to hass."""
|
"""Test helper to add entry to hass."""
|
||||||
|
@ -368,6 +370,14 @@ class MockUser(auth_models.User):
|
||||||
def add_to_auth_manager(self, auth_mgr):
|
def add_to_auth_manager(self, auth_mgr):
|
||||||
"""Test helper to add entry to hass."""
|
"""Test helper to add entry to hass."""
|
||||||
ensure_auth_manager_loaded(auth_mgr)
|
ensure_auth_manager_loaded(auth_mgr)
|
||||||
|
|
||||||
|
if self._mock_system_generated:
|
||||||
|
group_id = auth_mgr._store._system_user_group_id
|
||||||
|
else:
|
||||||
|
group_id = auth_mgr._store._default_new_user_group_id
|
||||||
|
|
||||||
|
self.group = auth_mgr._store._groups[group_id]
|
||||||
|
|
||||||
auth_mgr._store._users[self.id] = self
|
auth_mgr._store._users[self.id] = self
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -392,7 +402,7 @@ def ensure_auth_manager_loaded(auth_mgr):
|
||||||
"""Ensure an auth manager is considered loaded."""
|
"""Ensure an auth manager is considered loaded."""
|
||||||
store = auth_mgr._store
|
store = auth_mgr._store
|
||||||
if store._users is None:
|
if store._users is None:
|
||||||
store._users = OrderedDict()
|
store._set_defaults()
|
||||||
|
|
||||||
|
|
||||||
class MockModule:
|
class MockModule:
|
||||||
|
|
|
@ -83,6 +83,7 @@ async def test_list(hass, hass_ws_client):
|
||||||
'is_owner': True,
|
'is_owner': True,
|
||||||
'is_active': True,
|
'is_active': True,
|
||||||
'system_generated': False,
|
'system_generated': False,
|
||||||
|
'group_id': owner.group.id,
|
||||||
'credentials': [{'type': 'homeassistant'}]
|
'credentials': [{'type': 'homeassistant'}]
|
||||||
}
|
}
|
||||||
assert data[1] == {
|
assert data[1] == {
|
||||||
|
@ -91,6 +92,7 @@ async def test_list(hass, hass_ws_client):
|
||||||
'is_owner': False,
|
'is_owner': False,
|
||||||
'is_active': True,
|
'is_active': True,
|
||||||
'system_generated': True,
|
'system_generated': True,
|
||||||
|
'group_id': system.group.id,
|
||||||
'credentials': [],
|
'credentials': [],
|
||||||
}
|
}
|
||||||
assert data[2] == {
|
assert data[2] == {
|
||||||
|
@ -99,6 +101,7 @@ async def test_list(hass, hass_ws_client):
|
||||||
'is_owner': False,
|
'is_owner': False,
|
||||||
'is_active': False,
|
'is_active': False,
|
||||||
'system_generated': False,
|
'system_generated': False,
|
||||||
|
'group_id': inactive.group.id,
|
||||||
'credentials': [],
|
'credentials': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +204,7 @@ async def test_create(hass, hass_ws_client, hass_access_token):
|
||||||
assert user.name == data_user['name']
|
assert user.name == data_user['name']
|
||||||
assert user.is_active
|
assert user.is_active
|
||||||
assert not user.is_owner
|
assert not user.is_owner
|
||||||
assert not user.system_generated
|
assert not user.group.system_generated
|
||||||
|
|
||||||
|
|
||||||
async def test_create_requires_owner(hass, hass_ws_client, hass_access_token):
|
async def test_create_requires_owner(hass, hass_ws_client, hass_access_token):
|
||||||
|
|
|
@ -105,7 +105,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock,
|
||||||
hass_storage[STORAGE_KEY]['data']['hassio_user']
|
hass_storage[STORAGE_KEY]['data']['hassio_user']
|
||||||
)
|
)
|
||||||
assert hassio_user is not None
|
assert hassio_user is not None
|
||||||
assert hassio_user.system_generated
|
assert hassio_user.group.system_generated
|
||||||
for token in hassio_user.refresh_tokens.values():
|
for token in hassio_user.refresh_tokens.values():
|
||||||
if token.token == refresh_token:
|
if token.token == refresh_token:
|
||||||
break
|
break
|
||||||
|
|
51
tests/fixtures/auth_v1.json
vendored
Normal file
51
tests/fixtures/auth_v1.json
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"credentials": [],
|
||||||
|
"refresh_tokens": [
|
||||||
|
{
|
||||||
|
"access_token_expiration": 1800.0,
|
||||||
|
"client_id": "http://localhost:8123/",
|
||||||
|
"created_at": "2018-09-28T10:35:47.969734+00:00",
|
||||||
|
"id": "aaaaf7deb7ab454685d33c50c319ae7e",
|
||||||
|
"jwt_key": "some-jwt-key",
|
||||||
|
"token": "some-token",
|
||||||
|
"user_id": "74f25fa868d649bf83114b321f5f0256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access_token_expiration": 1800.0,
|
||||||
|
"client_id": "http://localhost:8123/",
|
||||||
|
"created_at": "2018-09-28T10:35:47.969734+00:00",
|
||||||
|
"id": "bbbbf7deb7ab454685d33c50c319ae7e",
|
||||||
|
"token": "some-token",
|
||||||
|
"user_id": "74f25fa868d649bf83114b321f5f0256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"access_token_expiration": 1800.0,
|
||||||
|
"client_id": null,
|
||||||
|
"created_at": "2018-09-28T10:35:47.969734+00:00",
|
||||||
|
"id": "aaaaf7deb7ab454685d33c50c319ae7e",
|
||||||
|
"jwt_key": "some-jwt-key",
|
||||||
|
"token": "some-token",
|
||||||
|
"user_id": "74f25fa868d649bf83114b321f5f0256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "74f25fa868d649cd83114b321f5f0256",
|
||||||
|
"is_active": true,
|
||||||
|
"is_owner": true,
|
||||||
|
"name": "Paulus",
|
||||||
|
"system_generated": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b8e85152681d4611a13fb3ffe04b99f4",
|
||||||
|
"is_active": true,
|
||||||
|
"is_owner": false,
|
||||||
|
"name": "Hass.io",
|
||||||
|
"system_generated": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"key": "auth",
|
||||||
|
"version": 1
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue