parent
dd55ff87c8
commit
c3b1121d77
7 changed files with 186 additions and 5 deletions
|
@ -130,13 +130,16 @@ class AuthManager:
|
|||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
groups=[],
|
||||
)
|
||||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
group = (await self._store.async_get_groups())[0]
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
'groups': [group]
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
|
|
|
@ -13,6 +13,7 @@ from . import models
|
|||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
INITIAL_GROUP_NAME = 'All Access'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
|
@ -28,9 +29,18 @@ class AuthStore:
|
|||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users = None # type: Optional[Dict[str, models.User]]
|
||||
self._groups = None # type: Optional[Dict[str, models.Group]]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
|
||||
async def async_get_groups(self) -> List[models.Group]:
|
||||
"""Retrieve all users."""
|
||||
if self._groups is None:
|
||||
await self._async_load()
|
||||
assert self._groups is not None
|
||||
|
||||
return list(self._groups.values())
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
|
@ -51,14 +61,20 @@ class AuthStore:
|
|||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
||||
credentials: Optional[models.Credentials] = None,
|
||||
groups: Optional[List[models.Group]] = None) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
'name': name,
|
||||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
'groups': groups or [],
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
|
@ -219,19 +235,36 @@ class AuthStore:
|
|||
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.get('groups', []):
|
||||
groups[group_dict['id']] = models.Group(
|
||||
name=group_dict['name'],
|
||||
id=group_dict['id'],
|
||||
)
|
||||
|
||||
migrate_group = None
|
||||
|
||||
if not groups:
|
||||
migrate_group = models.Group(name=INITIAL_GROUP_NAME)
|
||||
groups[migrate_group.id] = migrate_group
|
||||
|
||||
for user_dict in data['users']:
|
||||
users[user_dict['id']] = models.User(
|
||||
name=user_dict['name'],
|
||||
groups=[groups[group_id] for group_id
|
||||
in user_dict.get('group_ids', [])],
|
||||
id=user_dict['id'],
|
||||
is_owner=user_dict['is_owner'],
|
||||
is_active=user_dict['is_active'],
|
||||
system_generated=user_dict['system_generated'],
|
||||
)
|
||||
if migrate_group is not None and not user_dict['system_generated']:
|
||||
users[user_dict['id']].groups = [migrate_group]
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
|
@ -286,6 +319,7 @@ class AuthStore:
|
|||
)
|
||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||
|
||||
self._groups = groups
|
||||
self._users = users
|
||||
|
||||
@callback
|
||||
|
@ -300,10 +334,12 @@ class AuthStore:
|
|||
def _data_to_save(self) -> Dict:
|
||||
"""Return the data to store."""
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'group_ids': [group.id for group in user.groups],
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
|
@ -312,6 +348,14 @@ class AuthStore:
|
|||
for user in self._users.values()
|
||||
]
|
||||
|
||||
groups = [
|
||||
{
|
||||
'name': group.name,
|
||||
'id': group.id,
|
||||
}
|
||||
for group in self._groups.values()
|
||||
]
|
||||
|
||||
credentials = [
|
||||
{
|
||||
'id': credential.id,
|
||||
|
@ -348,6 +392,7 @@ class AuthStore:
|
|||
|
||||
return {
|
||||
'users': users,
|
||||
'groups': groups,
|
||||
'credentials': credentials,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
|
@ -355,3 +400,11 @@ class AuthStore:
|
|||
def _set_defaults(self) -> None:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
# Add default group
|
||||
all_access_group = models.Group(name=INITIAL_GROUP_NAME)
|
||||
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
groups[all_access_group.id] = all_access_group
|
||||
|
||||
self._groups = groups
|
||||
|
|
|
@ -14,6 +14,14 @@ TOKEN_TYPE_SYSTEM = 'system'
|
|||
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, factory=lambda: uuid.uuid4().hex)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
@ -24,6 +32,8 @@ class User:
|
|||
is_active = attr.ib(type=bool, default=False)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group]
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(
|
||||
type=list, factory=list, cmp=False
|
||||
|
|
|
@ -92,6 +92,7 @@ def _user_info(user):
|
|||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'system_generated': user.system_generated,
|
||||
'group_ids': [group.id for group in user.groups],
|
||||
'credentials': [
|
||||
{
|
||||
'type': c.auth_provider_type,
|
||||
|
|
82
tests/auth/test_auth_store.py
Normal file
82
tests/auth/test_auth_store.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
"""Tests for the auth store."""
|
||||
from homeassistant.auth import auth_store
|
||||
|
||||
|
||||
async def test_loading_old_data_format(hass, hass_storage):
|
||||
"""Test we correctly load an old data format."""
|
||||
hass_storage[auth_store.STORAGE_KEY] = {
|
||||
'version': 1,
|
||||
'data': {
|
||||
'credentials': [],
|
||||
'users': [
|
||||
{
|
||||
"id": "user-id",
|
||||
"is_active": True,
|
||||
"is_owner": True,
|
||||
"name": "Paulus",
|
||||
"system_generated": False,
|
||||
},
|
||||
{
|
||||
"id": "system-id",
|
||||
"is_active": True,
|
||||
"is_owner": True,
|
||||
"name": "Hass.io",
|
||||
"system_generated": True,
|
||||
}
|
||||
],
|
||||
"refresh_tokens": [
|
||||
{
|
||||
"access_token_expiration": 1800.0,
|
||||
"client_id": "http://localhost:8123/",
|
||||
"created_at": "2018-10-03T13:43:19.774637+00:00",
|
||||
"id": "user-token-id",
|
||||
"jwt_key": "some-key",
|
||||
"last_used_at": "2018-10-03T13:43:19.774712+00:00",
|
||||
"token": "some-token",
|
||||
"user_id": "user-id"
|
||||
},
|
||||
{
|
||||
"access_token_expiration": 1800.0,
|
||||
"client_id": None,
|
||||
"created_at": "2018-10-03T13:43:19.774637+00:00",
|
||||
"id": "system-token-id",
|
||||
"jwt_key": "some-key",
|
||||
"last_used_at": "2018-10-03T13:43:19.774712+00:00",
|
||||
"token": "some-token",
|
||||
"user_id": "system-id"
|
||||
},
|
||||
{
|
||||
"access_token_expiration": 1800.0,
|
||||
"client_id": "http://localhost:8123/",
|
||||
"created_at": "2018-10-03T13:43:19.774637+00:00",
|
||||
"id": "hidden-because-no-jwt-id",
|
||||
"last_used_at": "2018-10-03T13:43:19.774712+00:00",
|
||||
"token": "some-token",
|
||||
"user_id": "user-id"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
store = auth_store.AuthStore(hass)
|
||||
groups = await store.async_get_groups()
|
||||
assert len(groups) == 1
|
||||
group = groups[0]
|
||||
assert group.name == "All Access"
|
||||
|
||||
users = await store.async_get_users()
|
||||
assert len(users) == 2
|
||||
|
||||
owner, system = users
|
||||
|
||||
assert owner.system_generated is False
|
||||
assert owner.groups == [group]
|
||||
assert len(owner.refresh_tokens) == 1
|
||||
owner_token = list(owner.refresh_tokens.values())[0]
|
||||
assert owner_token.id == 'user-token-id'
|
||||
|
||||
assert system.system_generated is True
|
||||
assert system.groups == []
|
||||
assert len(system.refresh_tokens) == 1
|
||||
system_token = list(system.refresh_tokens.values())[0]
|
||||
assert system_token.id == 'system-token-id'
|
|
@ -345,17 +345,42 @@ def mock_device_registry(hass, mock_entries=None):
|
|||
return registry
|
||||
|
||||
|
||||
class MockGroup(auth_models.Group):
|
||||
"""Mock a group in Home Assistant."""
|
||||
|
||||
def __init__(self, id=None, name='Mock Group'):
|
||||
"""Mock a group."""
|
||||
kwargs = {
|
||||
'name': name
|
||||
}
|
||||
if id is not None:
|
||||
kwargs['id'] = id
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_to_hass(self, hass):
|
||||
"""Test helper to add entry to hass."""
|
||||
return self.add_to_auth_manager(hass.auth)
|
||||
|
||||
def add_to_auth_manager(self, auth_mgr):
|
||||
"""Test helper to add entry to hass."""
|
||||
ensure_auth_manager_loaded(auth_mgr)
|
||||
auth_mgr._store._groups[self.id] = self
|
||||
return self
|
||||
|
||||
|
||||
class MockUser(auth_models.User):
|
||||
"""Mock a user in Home Assistant."""
|
||||
|
||||
def __init__(self, id=None, is_owner=False, is_active=True,
|
||||
name='Mock User', system_generated=False):
|
||||
name='Mock User', system_generated=False, groups=None):
|
||||
"""Initialize mock user."""
|
||||
kwargs = {
|
||||
'is_owner': is_owner,
|
||||
'is_active': is_active,
|
||||
'name': name,
|
||||
'system_generated': system_generated,
|
||||
'groups': groups or [],
|
||||
}
|
||||
if id is not None:
|
||||
kwargs['id'] = id
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from homeassistant.auth import models as auth_models
|
||||
from homeassistant.components.config import auth as auth_config
|
||||
|
||||
from tests.common import MockUser, CLIENT_ID
|
||||
from tests.common import MockGroup, MockUser, CLIENT_ID
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -39,10 +39,13 @@ async def test_list_requires_owner(hass, hass_ws_client, hass_access_token):
|
|||
|
||||
async def test_list(hass, hass_ws_client):
|
||||
"""Test get users."""
|
||||
group = MockGroup().add_to_hass(hass)
|
||||
|
||||
owner = MockUser(
|
||||
id='abc',
|
||||
name='Test Owner',
|
||||
is_owner=True,
|
||||
groups=[group],
|
||||
).add_to_hass(hass)
|
||||
|
||||
owner.credentials.append(auth_models.Credentials(
|
||||
|
@ -61,6 +64,7 @@ async def test_list(hass, hass_ws_client):
|
|||
id='hij',
|
||||
name='Inactive User',
|
||||
is_active=False,
|
||||
groups=[group],
|
||||
).add_to_hass(hass)
|
||||
|
||||
refresh_token = await hass.auth.async_create_refresh_token(
|
||||
|
@ -83,6 +87,7 @@ async def test_list(hass, hass_ws_client):
|
|||
'is_owner': True,
|
||||
'is_active': True,
|
||||
'system_generated': False,
|
||||
'group_ids': [group.id for group in owner.groups],
|
||||
'credentials': [{'type': 'homeassistant'}]
|
||||
}
|
||||
assert data[1] == {
|
||||
|
@ -91,6 +96,7 @@ async def test_list(hass, hass_ws_client):
|
|||
'is_owner': False,
|
||||
'is_active': True,
|
||||
'system_generated': True,
|
||||
'group_ids': [],
|
||||
'credentials': [],
|
||||
}
|
||||
assert data[2] == {
|
||||
|
@ -99,6 +105,7 @@ async def test_list(hass, hass_ws_client):
|
|||
'is_owner': False,
|
||||
'is_active': False,
|
||||
'system_generated': False,
|
||||
'group_ids': [group.id for group in inactive.groups],
|
||||
'credentials': [],
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue