Skip to content

Commit

Permalink
Merge pull request #16666 from home-assistant/rc
Browse files Browse the repository at this point in the history
0.78.0
  • Loading branch information
balloob committed Sep 17, 2018
2 parents 7a5e828 + 366e270 commit 9e59fc5
Show file tree
Hide file tree
Showing 269 changed files with 5,680 additions and 2,116 deletions.
16 changes: 12 additions & 4 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,14 @@ omit =
homeassistant/components/google.py
homeassistant/components/*/google.py

homeassistant/components/habitica/*
homeassistant/components/*/habitica.py

homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/*/hangouts.py
homeassistant/components/*/hangouts.py

homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
Expand All @@ -142,12 +145,12 @@ omit =

homeassistant/components/ihc/*
homeassistant/components/*/ihc.py

homeassistant/components/insteon/*
homeassistant/components/*/insteon.py

homeassistant/components/insteon_local.py

homeassistant/components/insteon_plm.py

homeassistant/components/ios.py
Expand Down Expand Up @@ -225,7 +228,7 @@ omit =
homeassistant/components/opencv.py
homeassistant/components/*/opencv.py

homeassistant/components/openuv.py
homeassistant/components/openuv/__init__.py
homeassistant/components/*/openuv.py

homeassistant/components/pilight.py
Expand Down Expand Up @@ -374,6 +377,7 @@ omit =
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/alarm_control_panel/totalconnect.py
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
homeassistant/components/apiai.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py
Expand Down Expand Up @@ -411,6 +415,7 @@ omit =
homeassistant/components/climate/honeywell.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/oem.py
homeassistant/components/climate/opentherm_gw.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/climate/sensibo.py
Expand Down Expand Up @@ -759,6 +764,7 @@ omit =
homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/volkszaehler.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py
Expand Down Expand Up @@ -789,6 +795,8 @@ omit =
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py
homeassistant/components/switch/switchbot.py
homeassistant/components/switch/switchmate.py
homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
Expand Down
395 changes: 201 additions & 194 deletions LICENSE.md

Large diffs are not rendered by default.

50 changes: 43 additions & 7 deletions homeassistant/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import asyncio
import logging
from collections import OrderedDict
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, cast

import jwt

from homeassistant import data_entry_flow
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util

Expand Down Expand Up @@ -242,8 +244,12 @@ async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
modules[module_id] = module.name
return modules

async def async_create_refresh_token(self, user: models.User,
client_id: Optional[str] = None) \
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
Expand All @@ -254,10 +260,36 @@ async def async_create_refresh_token(self, user: models.User,
'System generated users cannot have refresh tokens connected '
'to a client.')

if not user.system_generated and client_id is None:
if token_type is None:
if user.system_generated:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL

if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
'System generated users can only have system type '
'refresh tokens')

if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
raise ValueError('Client is required to generate a refresh token.')

return await self._store.async_create_refresh_token(user, client_id)
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
client_name is None):
raise ValueError('Client_name is required for long-lived access '
'token')

if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values():
if (token.client_name == client_name and token.token_type ==
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
# Each client_name can only have one
# long_lived_access_token type of refresh token
raise ValueError('{} already exists'.format(client_name))

return await self._store.async_create_refresh_token(
user, client_id, client_name, client_icon,
token_type, access_token_expiration)

async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
Expand All @@ -277,13 +309,17 @@ async def async_remove_refresh_token(self,

@callback
def async_create_access_token(self,
refresh_token: models.RefreshToken) -> str:
refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> str:
"""Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)

# pylint: disable=no-self-use
now = dt_util.utcnow()
return jwt.encode({
'iss': refresh_token.id,
'iat': dt_util.utcnow(),
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
'iat': now,
'exp': now + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()

async def async_validate_access_token(
Expand Down
60 changes: 57 additions & 3 deletions homeassistant/auth/auth_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Dict, List, Optional # noqa: F401
import hmac

from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util

Expand Down Expand Up @@ -128,11 +129,27 @@ async def async_remove_credentials(
self._async_schedule_save()

async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None) \
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
"""Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id)
kwargs = {
'user': user,
'client_id': client_id,
'token_type': token_type,
'access_token_expiration': access_token_expiration
} # type: Dict[str, Any]
if client_name:
kwargs['client_name'] = client_name
if client_icon:
kwargs['client_icon'] = client_icon

refresh_token = models.RefreshToken(**kwargs)
user.refresh_tokens[refresh_token.id] = refresh_token

self._async_schedule_save()
return refresh_token

Expand Down Expand Up @@ -178,6 +195,15 @@ async def async_get_refresh_token_by_token(

return found

@callback
def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> None:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip
self._async_schedule_save()

async def _async_load(self) -> None:
"""Load the users."""
data = await self._store.async_load()
Expand Down Expand Up @@ -216,15 +242,36 @@ async def _async_load(self) -> None:
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
continue

token_type = rt_dict.get('token_type')
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:
last_used_at = dt_util.parse_datetime(last_used_at_str)
else:
last_used_at = None

token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
# use dict.get to keep backward compatibility
client_name=rt_dict.get('client_name'),
client_icon=rt_dict.get('client_icon'),
token_type=token_type,
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key']
jwt_key=rt_dict['jwt_key'],
last_used_at=last_used_at,
last_used_ip=rt_dict.get('last_used_ip'),
)
users[rt_dict['user_id']].refresh_tokens[token.id] = token

Expand Down Expand Up @@ -271,11 +318,18 @@ def _data_to_save(self) -> Dict:
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'client_name': refresh_token.client_name,
'client_icon': refresh_token.client_icon,
'token_type': refresh_token.token_type,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
'last_used_at':
refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at else None,
'last_used_ip': refresh_token.last_used_ip,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
Expand Down
21 changes: 16 additions & 5 deletions homeassistant/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

from homeassistant.util import dt as dt_util

from .const import ACCESS_TOKEN_EXPIRATION
from .util import generate_secret

TOKEN_TYPE_NORMAL = 'normal'
TOKEN_TYPE_SYSTEM = 'system'
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'


@attr.s(slots=True)
class User:
Expand Down Expand Up @@ -37,23 +40,31 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""

user = attr.ib(type=User)
client_id = attr.ib(type=str) # type: Optional[str]
client_id = attr.ib(type=Optional[str])
access_token_expiration = attr.ib(type=timedelta)
client_name = attr.ib(type=Optional[str], default=None)
client_icon = attr.ib(type=Optional[str], default=None)
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_((
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta,
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))

last_used_at = attr.ib(type=Optional[datetime], default=None)
last_used_ip = attr.ib(type=Optional[str], default=None)


@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""

auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str) # type: Optional[str]
auth_provider_id = attr.ib(type=Optional[str])

# Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict)
Expand Down
18 changes: 8 additions & 10 deletions homeassistant/auth/providers/legacy_api_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)

LEGACY_USER = 'homeassistant'
LEGACY_USER_NAME = 'Legacy API password user'


class InvalidAuthError(HomeAssistantError):
Expand Down Expand Up @@ -52,23 +52,21 @@ def async_validate_login(self, password: str) -> None:

async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Return LEGACY_USER always."""
for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER:
return credential
"""Return credentials for this login."""
credentials = await self.async_credentials()
if credentials:
return credentials[0]

return self.async_create_credentials({
'username': LEGACY_USER
})
return self.async_create_credentials({})

async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""
Set name as LEGACY_USER always.
Return info for the user.
Will be used to populate info when creating a new user.
"""
return UserMeta(name=LEGACY_USER, is_active=True)
return UserMeta(name=LEGACY_USER_NAME, is_active=True)


class LegacyLoginFlow(LoginFlow):
Expand Down

0 comments on commit 9e59fc5

Please sign in to comment.