mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
System groups (#18303)
* Add read only and admin policies * Migrate to 2 system groups * Add system groups * Add system groups admin & read only * Dont' mutate parameters * Fix types
This commit is contained in:
parent
54b0cde52a
commit
f7f0a4e811
@ -13,6 +13,7 @@ from homeassistant.core import callback, HomeAssistant
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import auth_store, models
|
from . import auth_store, models
|
||||||
|
from .const import GROUP_ID_ADMIN
|
||||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ class AuthManager:
|
|||||||
name=name,
|
name=name,
|
||||||
system_generated=True,
|
system_generated=True,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
groups=[],
|
group_ids=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||||
@ -144,11 +145,10 @@ class AuthManager:
|
|||||||
|
|
||||||
async def async_create_user(self, name: str) -> models.User:
|
async def async_create_user(self, name: str) -> models.User:
|
||||||
"""Create a user."""
|
"""Create a user."""
|
||||||
group = (await self._store.async_get_groups())[0]
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'is_active': True,
|
'is_active': True,
|
||||||
'groups': [group]
|
'group_ids': [GROUP_ID_ADMIN]
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
if await self._user_should_be_owner():
|
if await self._user_should_be_owner():
|
||||||
|
@ -10,11 +10,14 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from .permissions import DEFAULT_POLICY
|
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
||||||
|
from .permissions import system_policies
|
||||||
|
from .permissions.types import PolicyType # noqa: F401
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_KEY = 'auth'
|
STORAGE_KEY = 'auth'
|
||||||
INITIAL_GROUP_NAME = 'All Access'
|
GROUP_NAME_ADMIN = 'Administrators'
|
||||||
|
GROUP_NAME_READ_ONLY = 'Read Only'
|
||||||
|
|
||||||
|
|
||||||
class AuthStore:
|
class AuthStore:
|
||||||
@ -63,7 +66,7 @@ class AuthStore:
|
|||||||
is_active: Optional[bool] = None,
|
is_active: Optional[bool] = None,
|
||||||
system_generated: Optional[bool] = None,
|
system_generated: Optional[bool] = None,
|
||||||
credentials: Optional[models.Credentials] = None,
|
credentials: Optional[models.Credentials] = None,
|
||||||
groups: Optional[List[models.Group]] = None) -> models.User:
|
group_ids: Optional[List[str]] = 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()
|
||||||
@ -71,11 +74,18 @@ class AuthStore:
|
|||||||
assert self._users is not None
|
assert self._users is not None
|
||||||
assert self._groups is not None
|
assert self._groups is not None
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
for group_id in (group_ids or []):
|
||||||
|
group = self._groups.get(group_id)
|
||||||
|
if group is None:
|
||||||
|
raise ValueError('Invalid group specified {}'.format(group_id))
|
||||||
|
groups.append(group)
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'name': name,
|
'name': name,
|
||||||
# Until we get group management, we just put everyone in the
|
# Until we get group management, we just put everyone in the
|
||||||
# same group.
|
# same group.
|
||||||
'groups': groups or [],
|
'groups': groups,
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
if is_owner is not None:
|
if is_owner is not None:
|
||||||
@ -238,38 +248,98 @@ class AuthStore:
|
|||||||
users = OrderedDict() # type: Dict[str, models.User]
|
users = OrderedDict() # type: Dict[str, models.User]
|
||||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||||
|
|
||||||
# When creating objects we mention each attribute explicetely. This
|
# Soft-migrating data as we load. We are going to make sure we have a
|
||||||
|
# read only group and an admin group. There are two states that we can
|
||||||
|
# migrate from:
|
||||||
|
# 1. Data from a recent version which has a single group without policy
|
||||||
|
# 2. Data from old version which has no groups
|
||||||
|
has_admin_group = False
|
||||||
|
has_read_only_group = False
|
||||||
|
group_without_policy = None
|
||||||
|
|
||||||
|
# When creating objects we mention each attribute explicitly. This
|
||||||
# prevents crashing if user rolls back HA version after a new property
|
# prevents crashing if user rolls back HA version after a new property
|
||||||
# was added.
|
# was added.
|
||||||
|
|
||||||
for group_dict in data.get('groups', []):
|
for group_dict in data.get('groups', []):
|
||||||
|
policy = None # type: Optional[PolicyType]
|
||||||
|
|
||||||
|
if group_dict['id'] == GROUP_ID_ADMIN:
|
||||||
|
has_admin_group = True
|
||||||
|
|
||||||
|
name = GROUP_NAME_ADMIN
|
||||||
|
policy = system_policies.ADMIN_POLICY
|
||||||
|
system_generated = True
|
||||||
|
|
||||||
|
elif group_dict['id'] == GROUP_ID_READ_ONLY:
|
||||||
|
has_read_only_group = True
|
||||||
|
|
||||||
|
name = GROUP_NAME_READ_ONLY
|
||||||
|
policy = system_policies.READ_ONLY_POLICY
|
||||||
|
system_generated = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
name = group_dict['name']
|
||||||
|
policy = group_dict.get('policy')
|
||||||
|
system_generated = False
|
||||||
|
|
||||||
|
# We don't want groups without a policy that are not system groups
|
||||||
|
# This is part of migrating from state 1
|
||||||
|
if policy is None:
|
||||||
|
group_without_policy = group_dict['id']
|
||||||
|
continue
|
||||||
|
|
||||||
groups[group_dict['id']] = models.Group(
|
groups[group_dict['id']] = models.Group(
|
||||||
name=group_dict['name'],
|
|
||||||
id=group_dict['id'],
|
id=group_dict['id'],
|
||||||
policy=group_dict.get('policy', DEFAULT_POLICY),
|
name=name,
|
||||||
|
policy=policy,
|
||||||
|
system_generated=system_generated,
|
||||||
)
|
)
|
||||||
|
|
||||||
migrate_group = None
|
# If there are no groups, add all existing users to the admin group.
|
||||||
|
# This is part of migrating from state 2
|
||||||
|
migrate_users_to_admin_group = (not groups and
|
||||||
|
group_without_policy is None)
|
||||||
|
|
||||||
if not groups:
|
# If we find a no_policy_group, we need to migrate all users to the
|
||||||
migrate_group = models.Group(
|
# admin group. We only do this if there are no other groups, as is
|
||||||
name=INITIAL_GROUP_NAME,
|
# the expected state. If not expected state, not marking people admin.
|
||||||
policy=DEFAULT_POLICY
|
# This is part of migrating from state 1
|
||||||
)
|
if groups and group_without_policy is not None:
|
||||||
groups[migrate_group.id] = migrate_group
|
group_without_policy = None
|
||||||
|
|
||||||
|
# This is part of migrating from state 1 and 2
|
||||||
|
if not has_admin_group:
|
||||||
|
admin_group = _system_admin_group()
|
||||||
|
groups[admin_group.id] = admin_group
|
||||||
|
|
||||||
|
# This is part of migrating from state 1 and 2
|
||||||
|
if not has_read_only_group:
|
||||||
|
read_only_group = _system_read_only_group()
|
||||||
|
groups[read_only_group.id] = read_only_group
|
||||||
|
|
||||||
for user_dict in data['users']:
|
for user_dict in data['users']:
|
||||||
|
# Collect the users group.
|
||||||
|
user_groups = []
|
||||||
|
for group_id in user_dict.get('group_ids', []):
|
||||||
|
# This is part of migrating from state 1
|
||||||
|
if group_id == group_without_policy:
|
||||||
|
group_id = GROUP_ID_ADMIN
|
||||||
|
user_groups.append(groups[group_id])
|
||||||
|
|
||||||
|
# This is part of migrating from state 2
|
||||||
|
if (not user_dict['system_generated'] and
|
||||||
|
migrate_users_to_admin_group):
|
||||||
|
user_groups.append(groups[GROUP_ID_ADMIN])
|
||||||
|
|
||||||
users[user_dict['id']] = models.User(
|
users[user_dict['id']] = models.User(
|
||||||
name=user_dict['name'],
|
name=user_dict['name'],
|
||||||
groups=[groups[group_id] for group_id
|
groups=user_groups,
|
||||||
in user_dict.get('group_ids', [])],
|
|
||||||
id=user_dict['id'],
|
id=user_dict['id'],
|
||||||
is_owner=user_dict['is_owner'],
|
is_owner=user_dict['is_owner'],
|
||||||
is_active=user_dict['is_active'],
|
is_active=user_dict['is_active'],
|
||||||
system_generated=user_dict['system_generated'],
|
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']:
|
for cred_dict in data['credentials']:
|
||||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||||
@ -356,11 +426,11 @@ class AuthStore:
|
|||||||
groups = []
|
groups = []
|
||||||
for group in self._groups.values():
|
for group in self._groups.values():
|
||||||
g_dict = {
|
g_dict = {
|
||||||
'name': group.name,
|
|
||||||
'id': group.id,
|
'id': group.id,
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
if group.policy is not DEFAULT_POLICY:
|
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
|
||||||
|
g_dict['name'] = group.name
|
||||||
g_dict['policy'] = group.policy
|
g_dict['policy'] = group.policy
|
||||||
|
|
||||||
groups.append(g_dict)
|
groups.append(g_dict)
|
||||||
@ -410,13 +480,29 @@ class AuthStore:
|
|||||||
"""Set default values for auth store."""
|
"""Set default values for auth store."""
|
||||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||||
|
|
||||||
# Add default group
|
|
||||||
all_access_group = models.Group(
|
|
||||||
name=INITIAL_GROUP_NAME,
|
|
||||||
policy=DEFAULT_POLICY,
|
|
||||||
)
|
|
||||||
|
|
||||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||||
groups[all_access_group.id] = all_access_group
|
admin_group = _system_admin_group()
|
||||||
|
groups[admin_group.id] = admin_group
|
||||||
|
read_only_group = _system_read_only_group()
|
||||||
|
groups[read_only_group.id] = read_only_group
|
||||||
self._groups = groups
|
self._groups = groups
|
||||||
|
|
||||||
|
|
||||||
|
def _system_admin_group() -> models.Group:
|
||||||
|
"""Create system admin group."""
|
||||||
|
return models.Group(
|
||||||
|
name=GROUP_NAME_ADMIN,
|
||||||
|
id=GROUP_ID_ADMIN,
|
||||||
|
policy=system_policies.ADMIN_POLICY,
|
||||||
|
system_generated=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _system_read_only_group() -> models.Group:
|
||||||
|
"""Create read only group."""
|
||||||
|
return models.Group(
|
||||||
|
name=GROUP_NAME_READ_ONLY,
|
||||||
|
id=GROUP_ID_READ_ONLY,
|
||||||
|
policy=system_policies.READ_ONLY_POLICY,
|
||||||
|
system_generated=True,
|
||||||
|
)
|
||||||
|
@ -3,3 +3,6 @@ from datetime import timedelta
|
|||||||
|
|
||||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||||
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||||
|
|
||||||
|
GROUP_ID_ADMIN = 'system-admin'
|
||||||
|
GROUP_ID_READ_ONLY = 'system-read-only'
|
||||||
|
@ -22,6 +22,7 @@ class Group:
|
|||||||
name = attr.ib(type=str) # type: Optional[str]
|
name = attr.ib(type=str) # type: Optional[str]
|
||||||
policy = attr.ib(type=perm_mdl.PolicyType)
|
policy = attr.ib(type=perm_mdl.PolicyType)
|
||||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||||
|
system_generated = attr.ib(type=bool, default=False)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
|
@ -7,18 +7,11 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
|
|
||||||
from .common import CategoryType, PolicyType
|
from .const import CAT_ENTITIES
|
||||||
|
from .types import CategoryType, PolicyType
|
||||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||||
from .merge import merge_policies # noqa
|
from .merge import merge_policies # noqa
|
||||||
|
|
||||||
|
|
||||||
# Default policy if group has no policy applied.
|
|
||||||
DEFAULT_POLICY = {
|
|
||||||
"entities": True
|
|
||||||
} # type: PolicyType
|
|
||||||
|
|
||||||
CAT_ENTITIES = 'entities'
|
|
||||||
|
|
||||||
POLICY_SCHEMA = vol.Schema({
|
POLICY_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||||
})
|
})
|
||||||
|
7
homeassistant/auth/permissions/const.py
Normal file
7
homeassistant/auth/permissions/const.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""Permission constants."""
|
||||||
|
CAT_ENTITIES = 'entities'
|
||||||
|
SUBCAT_ALL = 'all'
|
||||||
|
|
||||||
|
POLICY_READ = 'read'
|
||||||
|
POLICY_CONTROL = 'control'
|
||||||
|
POLICY_EDIT = 'edit'
|
@ -5,12 +5,8 @@ from typing import ( # noqa: F401
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .common import CategoryType, ValueType, SUBCAT_ALL
|
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
|
||||||
|
from .types import CategoryType, ValueType
|
||||||
|
|
||||||
POLICY_READ = 'read'
|
|
||||||
POLICY_CONTROL = 'control'
|
|
||||||
POLICY_EDIT = 'edit'
|
|
||||||
|
|
||||||
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
||||||
vol.Optional(POLICY_READ): True,
|
vol.Optional(POLICY_READ): True,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from typing import ( # noqa: F401
|
from typing import ( # noqa: F401
|
||||||
cast, Dict, List, Set)
|
cast, Dict, List, Set)
|
||||||
|
|
||||||
from .common import PolicyType, CategoryType
|
from .types import PolicyType, CategoryType
|
||||||
|
|
||||||
|
|
||||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||||
|
14
homeassistant/auth/permissions/system_policies.py
Normal file
14
homeassistant/auth/permissions/system_policies.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""System policies."""
|
||||||
|
from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ
|
||||||
|
|
||||||
|
ADMIN_POLICY = {
|
||||||
|
CAT_ENTITIES: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
READ_ONLY_POLICY = {
|
||||||
|
CAT_ENTITIES: {
|
||||||
|
SUBCAT_ALL: {
|
||||||
|
POLICY_READ: True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,5 +29,3 @@ CategoryType = Union[
|
|||||||
|
|
||||||
# Example: { entities: … }
|
# Example: { entities: … }
|
||||||
PolicyType = Mapping[str, CategoryType]
|
PolicyType = Mapping[str, CategoryType]
|
||||||
|
|
||||||
SUBCAT_ALL = 'all'
|
|
@ -32,15 +32,3 @@ def test_owner_permissions():
|
|||||||
State('light.balcony', 'on'),
|
State('light.balcony', 'on'),
|
||||||
]
|
]
|
||||||
assert permissions.OwnerPermissions.filter_states(states) == states
|
assert permissions.OwnerPermissions.filter_states(states) == states
|
||||||
|
|
||||||
|
|
||||||
def test_default_policy_allow_all():
|
|
||||||
"""Test that the default policy is to allow all entity actions."""
|
|
||||||
perm = permissions.PolicyPermissions(permissions.DEFAULT_POLICY)
|
|
||||||
assert perm.check_entity('light.kitchen', 'read')
|
|
||||||
states = [
|
|
||||||
State('light.kitchen', 'on'),
|
|
||||||
State('light.living_room', 'off'),
|
|
||||||
State('light.balcony', 'on'),
|
|
||||||
]
|
|
||||||
assert perm.filter_states(states) == states
|
|
||||||
|
25
tests/auth/permissions/test_system_policies.py
Normal file
25
tests/auth/permissions/test_system_policies.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""Test system policies."""
|
||||||
|
from homeassistant.auth.permissions import (
|
||||||
|
PolicyPermissions, system_policies, POLICY_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_policy():
|
||||||
|
"""Test admin policy works."""
|
||||||
|
# Make sure it's valid
|
||||||
|
POLICY_SCHEMA(system_policies.ADMIN_POLICY)
|
||||||
|
|
||||||
|
perms = PolicyPermissions(system_policies.ADMIN_POLICY)
|
||||||
|
assert perms.check_entity('light.kitchen', 'read')
|
||||||
|
assert perms.check_entity('light.kitchen', 'control')
|
||||||
|
assert perms.check_entity('light.kitchen', 'edit')
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_policy():
|
||||||
|
"""Test read only policy works."""
|
||||||
|
# Make sure it's valid
|
||||||
|
POLICY_SCHEMA(system_policies.READ_ONLY_POLICY)
|
||||||
|
|
||||||
|
perms = PolicyPermissions(system_policies.READ_ONLY_POLICY)
|
||||||
|
assert perms.check_entity('light.kitchen', 'read')
|
||||||
|
assert not perms.check_entity('light.kitchen', 'control')
|
||||||
|
assert not perms.check_entity('light.kitchen', 'edit')
|
@ -2,8 +2,8 @@
|
|||||||
from homeassistant.auth import auth_store
|
from homeassistant.auth import auth_store
|
||||||
|
|
||||||
|
|
||||||
async def test_loading_old_data_format(hass, hass_storage):
|
async def test_loading_no_group_data_format(hass, hass_storage):
|
||||||
"""Test we correctly load an old data format."""
|
"""Test we correctly load old data without any groups."""
|
||||||
hass_storage[auth_store.STORAGE_KEY] = {
|
hass_storage[auth_store.STORAGE_KEY] = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'data': {
|
'data': {
|
||||||
@ -60,9 +60,15 @@ async def test_loading_old_data_format(hass, hass_storage):
|
|||||||
|
|
||||||
store = auth_store.AuthStore(hass)
|
store = auth_store.AuthStore(hass)
|
||||||
groups = await store.async_get_groups()
|
groups = await store.async_get_groups()
|
||||||
assert len(groups) == 1
|
assert len(groups) == 2
|
||||||
group = groups[0]
|
admin_group = groups[0]
|
||||||
assert group.name == "All Access"
|
assert admin_group.name == auth_store.GROUP_NAME_ADMIN
|
||||||
|
assert admin_group.system_generated
|
||||||
|
assert admin_group.id == auth_store.GROUP_ID_ADMIN
|
||||||
|
read_group = groups[1]
|
||||||
|
assert read_group.name == auth_store.GROUP_NAME_READ_ONLY
|
||||||
|
assert read_group.system_generated
|
||||||
|
assert read_group.id == auth_store.GROUP_ID_READ_ONLY
|
||||||
|
|
||||||
users = await store.async_get_users()
|
users = await store.async_get_users()
|
||||||
assert len(users) == 2
|
assert len(users) == 2
|
||||||
@ -70,7 +76,7 @@ async def test_loading_old_data_format(hass, hass_storage):
|
|||||||
owner, system = users
|
owner, system = users
|
||||||
|
|
||||||
assert owner.system_generated is False
|
assert owner.system_generated is False
|
||||||
assert owner.groups == [group]
|
assert owner.groups == [admin_group]
|
||||||
assert len(owner.refresh_tokens) == 1
|
assert len(owner.refresh_tokens) == 1
|
||||||
owner_token = list(owner.refresh_tokens.values())[0]
|
owner_token = list(owner.refresh_tokens.values())[0]
|
||||||
assert owner_token.id == 'user-token-id'
|
assert owner_token.id == 'user-token-id'
|
||||||
@ -80,3 +86,126 @@ async def test_loading_old_data_format(hass, hass_storage):
|
|||||||
assert len(system.refresh_tokens) == 1
|
assert len(system.refresh_tokens) == 1
|
||||||
system_token = list(system.refresh_tokens.values())[0]
|
system_token = list(system.refresh_tokens.values())[0]
|
||||||
assert system_token.id == 'system-token-id'
|
assert system_token.id == 'system-token-id'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_loading_all_access_group_data_format(hass, hass_storage):
|
||||||
|
"""Test we correctly load old data with single group."""
|
||||||
|
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,
|
||||||
|
'group_ids': ['abcd-all-access']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "system-id",
|
||||||
|
"is_active": True,
|
||||||
|
"is_owner": True,
|
||||||
|
"name": "Hass.io",
|
||||||
|
"system_generated": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "abcd-all-access",
|
||||||
|
"name": "All Access",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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) == 2
|
||||||
|
admin_group = groups[0]
|
||||||
|
assert admin_group.name == auth_store.GROUP_NAME_ADMIN
|
||||||
|
assert admin_group.system_generated
|
||||||
|
assert admin_group.id == auth_store.GROUP_ID_ADMIN
|
||||||
|
read_group = groups[1]
|
||||||
|
assert read_group.name == auth_store.GROUP_NAME_READ_ONLY
|
||||||
|
assert read_group.system_generated
|
||||||
|
assert read_group.id == auth_store.GROUP_ID_READ_ONLY
|
||||||
|
|
||||||
|
users = await store.async_get_users()
|
||||||
|
assert len(users) == 2
|
||||||
|
|
||||||
|
owner, system = users
|
||||||
|
|
||||||
|
assert owner.system_generated is False
|
||||||
|
assert owner.groups == [admin_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'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_loading_empty_data(hass, hass_storage):
|
||||||
|
"""Test we correctly load with no existing data."""
|
||||||
|
store = auth_store.AuthStore(hass)
|
||||||
|
groups = await store.async_get_groups()
|
||||||
|
assert len(groups) == 2
|
||||||
|
admin_group = groups[0]
|
||||||
|
assert admin_group.name == auth_store.GROUP_NAME_ADMIN
|
||||||
|
assert admin_group.system_generated
|
||||||
|
assert admin_group.id == auth_store.GROUP_ID_ADMIN
|
||||||
|
read_group = groups[1]
|
||||||
|
assert read_group.name == auth_store.GROUP_NAME_READ_ONLY
|
||||||
|
assert read_group.system_generated
|
||||||
|
assert read_group.id == auth_store.GROUP_ID_READ_ONLY
|
||||||
|
|
||||||
|
users = await store.async_get_users()
|
||||||
|
assert len(users) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_system_groups_only_store_id(hass, hass_storage):
|
||||||
|
"""Test that for system groups we only store the ID."""
|
||||||
|
store = auth_store.AuthStore(hass)
|
||||||
|
await store._async_load()
|
||||||
|
data = store._data_to_save()
|
||||||
|
assert len(data['users']) == 0
|
||||||
|
assert data['groups'] == [
|
||||||
|
{'id': auth_store.GROUP_ID_ADMIN},
|
||||||
|
{'id': auth_store.GROUP_ID_READ_ONLY},
|
||||||
|
]
|
||||||
|
@ -15,6 +15,7 @@ from contextlib import contextmanager
|
|||||||
from homeassistant import auth, core as ha, config_entries
|
from homeassistant import auth, core as ha, config_entries
|
||||||
from homeassistant.auth import (
|
from homeassistant.auth import (
|
||||||
models as auth_models, auth_store, providers as auth_providers)
|
models as auth_models, auth_store, providers as auth_providers)
|
||||||
|
from homeassistant.auth.permissions import system_policies
|
||||||
from homeassistant.setup import setup_component, async_setup_component
|
from homeassistant.setup import setup_component, async_setup_component
|
||||||
from homeassistant.config import async_process_component_config
|
from homeassistant.config import async_process_component_config
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
@ -349,7 +350,7 @@ class MockGroup(auth_models.Group):
|
|||||||
"""Mock a group in Home Assistant."""
|
"""Mock a group in Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, id=None, name='Mock Group',
|
def __init__(self, id=None, name='Mock Group',
|
||||||
policy=auth_store.DEFAULT_POLICY):
|
policy=system_policies.ADMIN_POLICY):
|
||||||
"""Mock a group."""
|
"""Mock a group."""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user