Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions homeassistant/auth/providers/homeassistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ class HassAuthProvider(AuthProvider):

async def async_initialize(self):
"""Initialize the auth provider."""
if self.data is not None:
return

self.data = Data(self.hass)
await self.data.async_load()

Expand Down
14 changes: 12 additions & 2 deletions homeassistant/components/frontend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
REQUIREMENTS = ['home-assistant-frontend==20180716.0']

DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', 'onboarding']

CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
Expand Down Expand Up @@ -377,6 +377,16 @@ async def get(self, request, extra=None):
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)

if not hass.components.onboarding.async_is_onboarded():
if latest:
location = '/frontend_latest/onboarding.html'
else:
location = '/frontend_es5/onboarding.html'

return web.Response(status=302, headers={
'location': location
})

no_auth = '1'
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
Expand Down Expand Up @@ -480,7 +490,7 @@ def websocket_get_translations(hass, connection, msg):
Async friendly.
"""
async def send_translations():
"""Send a camera still."""
"""Send a translation."""
resources = await async_get_translations(hass, msg['language'])
connection.send_message_outside(websocket_api.result_message(
msg['id'], {
Expand Down
56 changes: 56 additions & 0 deletions homeassistant/components/onboarding/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Component to help onboard new users."""
from homeassistant.core import callback
from homeassistant.loader import bind_hass

from .const import STEPS, STEP_USER, DOMAIN

DEPENDENCIES = ['http']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1


@bind_hass
@callback
def async_is_onboarded(hass):
"""Return if Home Assistant has been onboarded."""
# Temporarily: if auth not active, always set onboarded=True
if not hass.auth.active:
return True

return hass.data.get(DOMAIN, True)
Copy link
Copy Markdown
Contributor

@awarecan awarecan Jul 17, 2018

Choose a reason for hiding this comment

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

Should check if owner already created

if hass.auth.has_owner():
    return True

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We do those checks inside the async_setup. We're not checking explicitly for owner, but instead check if the user step is done. There can be many steps in the future.



async def async_setup(hass, config):
"""Set up the onboard component."""
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
data = await store.async_load()

if data is None:
data = {
'done': []
}

if STEP_USER not in data['done']:
# Users can already have created an owner account via the command line
# If so, mark the user step as done.
has_owner = False

for user in await hass.auth.async_get_users():
if user.is_owner:
has_owner = True
break

if has_owner:
data['done'].append(STEP_USER)
await store.async_save(data)

if set(data['done']) == set(STEPS):
return True

hass.data[DOMAIN] = False

from . import views

await views.async_setup(hass, data, store)

return True
7 changes: 7 additions & 0 deletions homeassistant/components/onboarding/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Constants for the onboarding component."""
DOMAIN = 'onboarding'
STEP_USER = 'user'

STEPS = [
STEP_USER
]
106 changes: 106 additions & 0 deletions homeassistant/components/onboarding/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Onboarding views."""
import asyncio

import voluptuous as vol

from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator

from .const import DOMAIN, STEPS, STEP_USER


async def async_setup(hass, data, store):
"""Setup onboarding."""
hass.http.register_view(OnboardingView(data, store))
hass.http.register_view(UserOnboardingView(data, store))


class OnboardingView(HomeAssistantView):
"""Returns the onboarding status."""

requires_auth = False
url = '/api/onboarding'
name = 'api:onboarding'

def __init__(self, data, store):
"""Initialize the onboarding view."""
self._store = store
self._data = data

async def get(self, request):
"""Return the onboarding status."""
return self.json([
{
'step': key,
'done': key in self._data['done'],
} for key in STEPS
])


class _BaseOnboardingView(HomeAssistantView):
"""Base class for onboarding."""

requires_auth = False
step = None

def __init__(self, data, store):
"""Initialize the onboarding view."""
self._store = store
self._data = data
self._lock = asyncio.Lock()

@callback
def _async_is_done(self):
"""Return if this step is done."""
return self.step in self._data['done']

async def _async_mark_done(self, hass):
"""Mark step as done."""
self._data['done'].append(self.step)
await self._store.async_save(self._data)

hass.data[DOMAIN] = len(self._data) == len(STEPS)


class UserOnboardingView(_BaseOnboardingView):
"""View to handle onboarding."""

url = '/api/onboarding/users'
name = 'api:onboarding:users'
step = STEP_USER

@RequestDataValidator(vol.Schema({
vol.Required('name'): str,
vol.Required('username'): str,
vol.Required('password'): str,
}))
async def post(self, request, data):
"""Return the manifest.json."""
hass = request.app['hass']

async with self._lock:
if self._async_is_done():
return self.json_message('User step already done', 403)

provider = _async_get_hass_provider(hass)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe in the future we can allow the first user coming from other provider by adding setup_shema to the provider.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We could but I don't know if I would want to . If people want to configure their own auth providers, surely they will skip onboarding?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Then we need to check if owner already created in async_is_onboarded()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's not a use case I am currently worried about.

Copy link
Copy Markdown
Contributor

@awarecan awarecan Jul 17, 2018

Choose a reason for hiding this comment

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

I am thinking the migration problem for current 'new auth' user, they have the need to skip onboarding user step.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a good point. If an owner exists I'll mark the user step as done.

await provider.async_initialize()

user = await hass.auth.async_create_user(data['name'])
await hass.async_add_executor_job(
provider.data.add_auth, data['username'], data['password'])
credentials = await provider.async_get_or_create_credentials({
'username': data['username']
})
await hass.auth.async_link_user(user, credentials)
await self._async_mark_done(hass)


@callback
def _async_get_hass_provider(hass):
"""Get the Home Assistant auth provider."""
for prv in hass.auth.auth_providers:
if prv.type == 'homeassistant':
return prv

raise RuntimeError('No Home Assistant provider found')
11 changes: 11 additions & 0 deletions tests/components/onboarding/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Tests for the onboarding component."""

from homeassistant.components import onboarding


def mock_storage(hass_storage, data):
"""Mock the onboarding storage."""
hass_storage[onboarding.STORAGE_KEY] = {
'version': onboarding.STORAGE_VERSION,
'data': data
}
77 changes: 77 additions & 0 deletions tests/components/onboarding/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Tests for the init."""
from unittest.mock import patch, Mock

from homeassistant.setup import async_setup_component
from homeassistant.components import onboarding

from tests.common import mock_coro, MockUser

from . import mock_storage

# Temporarily: if auth not active, always set onboarded=True


async def test_not_setup_views_if_onboarded(hass, hass_storage):
"""Test if onboarding is done, we don't setup views."""
mock_storage(hass_storage, {
'done': onboarding.STEPS
})

with patch(
'homeassistant.components.onboarding.views.async_setup'
) as mock_setup:
assert await async_setup_component(hass, 'onboarding', {})

assert len(mock_setup.mock_calls) == 0
assert onboarding.DOMAIN not in hass.data
assert onboarding.async_is_onboarded(hass)


async def test_setup_views_if_not_onboarded(hass):
"""Test if onboarding is not done, we setup views."""
with patch(
'homeassistant.components.onboarding.views.async_setup',
return_value=mock_coro()
) as mock_setup:
assert await async_setup_component(hass, 'onboarding', {})

assert len(mock_setup.mock_calls) == 1
assert onboarding.DOMAIN in hass.data

with patch('homeassistant.auth.AuthManager.active', return_value=True):
assert not onboarding.async_is_onboarded(hass)


async def test_is_onboarded():
"""Test the is onboarded function."""
hass = Mock()
hass.data = {}

with patch('homeassistant.auth.AuthManager.active', return_value=False):
assert onboarding.async_is_onboarded(hass)

with patch('homeassistant.auth.AuthManager.active', return_value=True):
assert onboarding.async_is_onboarded(hass)

hass.data[onboarding.DOMAIN] = True
assert onboarding.async_is_onboarded(hass)

hass.data[onboarding.DOMAIN] = False
assert not onboarding.async_is_onboarded(hass)


async def test_having_owner_finishes_user_step(hass, hass_storage):
"""If owner user already exists, mark user step as complete."""
MockUser(is_owner=True).add_to_hass(hass)

with patch(
'homeassistant.components.onboarding.views.async_setup'
) as mock_setup, patch.object(onboarding, 'STEPS', [onboarding.STEP_USER]):
assert await async_setup_component(hass, 'onboarding', {})

assert len(mock_setup.mock_calls) == 0
assert onboarding.DOMAIN not in hass.data
assert onboarding.async_is_onboarded(hass)

done = hass_storage[onboarding.STORAGE_KEY]['data']['done']
assert onboarding.STEP_USER in done
Loading