Add group foundation (#16935)

Add group foundation
This commit is contained in:
Paulus Schoutsen 2018-10-08 16:35:38 +02:00 committed by GitHub
parent dd55ff87c8
commit c3b1121d77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 5 deletions

View file

@ -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():

View file

@ -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

View file

@ -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

View file

@ -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,

View 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'

View file

@ -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

View file

@ -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': [],
}