Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions homeassistant/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,6 @@ async def async_enable_user_mfa(self, user: models.User,
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))

if module.setup_schema is not None:
try:
# pylint: disable=not-callable
data = module.setup_schema(data)
except vol.Invalid as err:
raise ValueError('Data does not match schema: {}'.format(err))

await module.async_setup_user(user.id, data)

async def async_disable_user_mfa(self, user: models.User,
Expand Down
49 changes: 42 additions & 7 deletions homeassistant/auth/mfa_modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import voluptuous as vol
from voluptuous.humanize import humanize_error

from homeassistant import requirements
from homeassistant import requirements, data_entry_flow
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.util.decorator import Registry
Expand Down Expand Up @@ -64,15 +64,14 @@ def input_schema(self) -> vol.Schema:
"""Return a voluptuous schema to define mfa auth module's input."""
raise NotImplementedError

@property
def setup_schema(self) -> Optional[vol.Schema]:
"""Return a vol schema to validate mfa auth module's setup input.
async def async_setup_flow(self, user_id: str) -> 'SetupFlow':
"""Return a data entry flow handler for setup module.

Optional
Mfa module should extend SetupFlow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is optional, no?

@awarecan awarecan Aug 24, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: it is required. MFA module has to be set up to enable.

"""
return None
raise NotImplementedError

async def async_setup_user(self, user_id: str, setup_data: Any) -> None:
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up user for mfa auth module."""
raise NotImplementedError

Expand All @@ -90,6 +89,42 @@ async def async_validation(
raise NotImplementedError


class SetupFlow(data_entry_flow.FlowHandler):
"""Handler for the setup flow."""

def __init__(self, auth_module: MultiFactorAuthModule,
setup_schema: vol.Schema,
user_id: str) -> None:
"""Initialize the setup flow."""
self._auth_module = auth_module
self._setup_schema = setup_schema
self._user_id = user_id

async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the first step of setup flow.

Return self.async_show_form(step_id='init') if user_input == None.
Return await self.async_finish(flow_result) if finish.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment?

"""
errors = {} # type: Dict[str, str]

if user_input:
result = await self._auth_module.async_setup_user(
self._user_id, user_input)
return self.async_create_entry(
title=self._auth_module.name,
data={'result': result}
)

return self.async_show_form(
step_id='init',
data_schema=self._setup_schema,
errors=errors
)


async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any]) \
-> Optional[MultiFactorAuthModule]:
Expand Down
13 changes: 10 additions & 3 deletions homeassistant/auth/mfa_modules/insecure_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from homeassistant.core import HomeAssistant

from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow

CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
vol.Required('data'): [vol.Schema({
Expand Down Expand Up @@ -36,11 +36,18 @@ def input_schema(self) -> vol.Schema:
return vol.Schema({'pin': str})

@property
def setup_schema(self) -> Optional[vol.Schema]:
def setup_schema(self) -> vol.Schema:
"""Validate async_setup_user input data."""
return vol.Schema({'pin': str})

async def async_setup_user(self, user_id: str, setup_data: Any) -> None:
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.

Mfa module should extend SetupFlow
"""
return SetupFlow(self, self.setup_schema, user_id)

async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up user to use mfa module."""
# data shall has been validate in caller
pin = setup_data['pin']
Expand Down
47 changes: 29 additions & 18 deletions homeassistant/components/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,16 @@
from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util

from . import indieauth
from . import login_flow
from . import mfa_setup_flow
from . import util

DOMAIN = 'auth'
DEPENDENCIES = ['http']
DEPENDENCIES = ['http', 'websocket_api']

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Websocket API commands can be registered without loading the websocket API. It is not a dependency.

If the user wants the websocket API, the commands will be available.


WS_TYPE_CURRENT_USER = 'auth/current_user'
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
Expand All @@ -100,6 +103,7 @@ async def async_setup(hass, config):
)

await login_flow.async_setup(hass, store_result)
await mfa_setup_flow.async_setup(hass)

return True

Expand Down Expand Up @@ -315,21 +319,28 @@ def retrieve_result(client_id, result_type, code):
return store_result, retrieve_result


@util.validate_current_user()
@callback
def websocket_current_user(hass, connection, msg):
def websocket_current_user(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Return the current user."""
user = connection.request.get('hass_user')

if user is None:
connection.to_write.put_nowait(websocket_api.error_message(
msg['id'], 'no_user', 'Not authenticated as a user'))
return

connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials]
}))
async def async_get_current_user(user):
"""Get current user."""
enabled_modules = await hass.auth.async_get_enabled_mfa(user)

connection.send_message_outside(
websocket_api.result_message(msg['id'], {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials],
'mfa_modules': [{
'id': module.id,
'name': module.name,
'enabled': module.id in enabled_modules,
} for module in hass.auth.auth_mfa_modules],
}))

hass.async_create_task(async_get_current_user(connection.user))
136 changes: 136 additions & 0 deletions homeassistant/components/auth/mfa_setup_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Helpers to setup multi-factor auth module."""
import logging

import voluptuous as vol

from homeassistant import data_entry_flow
from homeassistant.components import websocket_api
from homeassistant.core import callback, HomeAssistant

from . import util

WS_TYPE_SETUP_MFA = 'auth/setup_mfa'
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SETUP_MFA,
vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str,
vol.Exclusive('flow_id', 'module_or_flow_id'): str,
vol.Optional('user_input'): object,
})

WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa'
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DEPOSE_MFA,
vol.Required('mfa_module_id'): str,
})

DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager'

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass):
"""Init mfa setup flow manager."""
async def _async_create_setup_flow(handler, context, data):
"""Create a setup flow. hanlder is a mfa module."""
mfa_module = hass.auth.get_auth_mfa_module(handler)
if mfa_module is None:
raise ValueError('Mfa module {} is not found'.format(handler))

user_id = data.pop('user_id')
return await mfa_module.async_setup_flow(user_id)

async def _async_finish_setup_flow(flow, flow_result):
_LOGGER.debug('flow_result: %s', flow_result)
return flow_result

hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
hass, _async_create_setup_flow, _async_finish_setup_flow)

hass.components.websocket_api.async_register_command(
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA)

hass.components.websocket_api.async_register_command(
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA)


@callback
@util.validate_current_user(allow_system_user=False)
def websocket_setup_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Return a setup flow for mfa auth module."""
async def async_setup_flow(msg):
"""Helper to return a setup flow for mfa auth module."""
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]

flow_id = msg.get('flow_id')
if flow_id is not None:
result = await flow_manager.async_configure(
flow_id, msg.get('user_input'))
connection.send_message_outside(
websocket_api.result_message(
msg['id'], _prepare_result_json(result)))
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside this if, add the write and return. That way you can drop the else

mfa_module_id = msg.get('mfa_module_id')
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
if mfa_module is None:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'no_module',
'MFA module {} is not found'.format(mfa_module_id)))
return

result = await flow_manager.async_init(
mfa_module_id, data={'user_id': connection.user.id})

connection.send_message_outside(
websocket_api.result_message(
msg['id'], _prepare_result_json(result)))

hass.async_create_task(async_setup_flow(msg))


@callback
@util.validate_current_user(allow_system_user=False)
def websocket_depose_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Remove user from mfa module."""
async def async_depose(msg):
"""Helper to disable user from mfa auth module."""
mfa_module_id = msg['mfa_module_id']
try:
await hass.auth.async_disable_user_mfa(
connection.user, msg['mfa_module_id'])
except ValueError as err:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'disable_failed',
'Cannot disable MFA Module {}: {}'.format(
mfa_module_id, err)))
return

connection.send_message_outside(
websocket_api.result_message(
msg['id'], 'done'))

hass.async_create_task(async_depose(msg))


def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
return data

if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result

import voluptuous_serialize

data = result.copy()

schema = data['data_schema']
if schema is None:
data['data_schema'] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)

return data
61 changes: 61 additions & 0 deletions homeassistant/components/auth/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Auth component utils."""
from functools import wraps

from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant


def validate_current_user(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have ws in the name. What about ws_require_user ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this method should be part of the websocket component.

only_owner=False, only_system_user=False, allow_system_user=True,
only_active_user=True, only_inactive_user=False):
"""Decorator that will validate login user exist in current WS connection.

Will write out error message if not authenticated.
"""
def validator(func):
"""Decorator be called."""
@wraps(func)
def check_current_user(hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg):
"""Check current user."""
def output_error(message_id, message):
"""Output error message."""
connection.send_message_outside(websocket_api.error_message(
msg['id'], message_id, message))

if connection.user is None:
output_error('no_user', 'Not authenticated as a user')
return

if only_owner and not connection.user.is_owner:
output_error('only_owner', 'Only allowed as owner')
return

if (only_system_user and
not connection.user.system_generated):
output_error('only_system_user',
'Only allowed as system user')
return

if (not allow_system_user
and connection.user.system_generated):
output_error('not_system_user', 'Not allowed as system user')
return

if (only_active_user and
not connection.user.is_active):
output_error('only_active_user',
'Only allowed as active user')
return

if only_inactive_user and connection.user.is_active:
output_error('only_inactive_user',
'Not allowed as active user')
return

return func(hass, connection, msg)

return check_current_user

return validator
Loading