Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate http.api_password #21884

Merged
merged 4 commits into from Mar 11, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 18 additions & 1 deletion homeassistant/auth/__init__.py
Expand Up @@ -100,9 +100,21 @@ def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())

def get_auth_provider(self, provider_type: str, provider_id: str) \
-> Optional[AuthProvider]:
"""Return an auth provider, None if not found."""
return self._providers.get((provider_type, provider_id))

def get_auth_providers(self, provider_type: str) \
-> List[AuthProvider]:
"""Return a List of auth provider of one type, Empty if not found."""
return [provider
for (p_type, _), provider in self._providers.items()
if p_type == provider_type]

def get_auth_mfa_module(self, module_id: str) \
-> Optional[MultiFactorAuthModule]:
"""Return an multi-factor auth module, None if not found."""
"""Return a multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)

async def async_get_users(self) -> List[models.User]:
Expand All @@ -113,6 +125,11 @@ async def async_get_user(self, user_id: str) -> Optional[models.User]:
"""Retrieve a user."""
return await self._store.async_get_user(user_id)

async def async_get_owner(self) -> Optional[models.User]:
"""Retrieve the owner."""
users = await self.async_get_users()
return next((user for user in users if user.is_owner), None)

async def async_get_group(self, group_id: str) -> Optional[models.Group]:
"""Retrieve all groups."""
return await self._store.async_get_group(group_id)
Expand Down
61 changes: 28 additions & 33 deletions homeassistant/auth/providers/legacy_api_password.py
Expand Up @@ -4,27 +4,23 @@
It will be removed when auth system production ready
"""
import hmac
from typing import Any, Dict, Optional, cast, TYPE_CHECKING
from typing import Any, Dict, Optional, cast

import voluptuous as vol

from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv

from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from .. import AuthManager
from ..models import Credentials, UserMeta, User

if TYPE_CHECKING:
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401


USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
})

AUTH_PROVIDER_TYPE = 'legacy_api_password'
CONF_API_PASSWORD = 'api_password'

CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required(CONF_API_PASSWORD): cv.string,
}, extra=vol.PREVENT_EXTRA)

LEGACY_USER_NAME = 'Legacy API password user'
Expand All @@ -34,40 +30,45 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""


async def async_get_user(hass: HomeAssistant) -> User:
"""Return the legacy API password user."""
async def async_validate_password(hass: HomeAssistant, password: str)\
-> Optional[User]:
"""Return a user if password is valid. None if not."""
auth = cast(AuthManager, hass.auth) # type: ignore
found = None

for prv in auth.auth_providers:
if prv.type == 'legacy_api_password':
found = prv
break

if found is None:
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
if not providers:
raise ValueError('Legacy API password provider not found')

return await auth.async_get_or_create_user(
await found.async_get_or_create_credentials({})
)
try:
provider = cast(LegacyApiPasswordAuthProvider, providers[0])
provider.async_validate_login(password)
return await auth.async_get_or_create_user(
await provider.async_get_or_create_credentials({})
)
except InvalidAuthError:
return None


@AUTH_PROVIDERS.register('legacy_api_password')
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
"""An auth provider support legacy api_password."""

DEFAULT_TITLE = 'Legacy API Password'

@property
def api_password(self) -> str:
"""Return api_password."""
return str(self.config[CONF_API_PASSWORD])

async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LegacyLoginFlow(self)

@callback
def async_validate_login(self, password: str) -> None:
"""Validate a username and password."""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
"""Validate password."""
api_password = str(self.config[CONF_API_PASSWORD])

if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
if not hmac.compare_digest(api_password.encode('utf-8'),
password.encode('utf-8')):
raise InvalidAuthError

Expand Down Expand Up @@ -99,12 +100,6 @@ async def async_step_init(
"""Handle the step of the form."""
errors = {}

hass_http = getattr(self.hass, 'http', None)
if hass_http is None or not hass_http.api_password:
return self.async_abort(
reason='no_api_password_set'
)

if user_input is not None:
try:
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/bootstrap.py
Expand Up @@ -99,12 +99,12 @@ async def async_from_config_dict(config: Dict[str, Any],
"This may cause issues")

core_config = config.get(core.DOMAIN, {})
has_api_password = bool(config.get('http', {}).get('api_password'))
api_password = config.get('http', {}).get('api_password')
trusted_networks = config.get('http', {}).get('trusted_networks')

try:
await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password, trusted_networks)
hass, core_config, api_password, trusted_networks)
except vol.Invalid as config_err:
conf_util.async_log_exception(
config_err, 'homeassistant', core_config, hass)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/__init__.py
Expand Up @@ -166,6 +166,7 @@ async def async_handle_reload_config(call):
_LOGGER.error(err)
return

# auth only processed during startup
await conf_util.async_process_ha_core_config(
hass, conf.get(ha.DOMAIN) or {})

Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/api/__init__.py
Expand Up @@ -168,11 +168,11 @@ class APIDiscoveryView(HomeAssistantView):
def get(self, request):
"""Get discovery information."""
hass = request.app['hass']
needs_auth = hass.config.api.api_password is not None
return self.json({
ATTR_BASE_URL: hass.config.api.base_url,
ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_REQUIRES_API_PASSWORD: needs_auth,
# always needs authentication
ATTR_REQUIRES_API_PASSWORD: True,
ATTR_VERSION: __version__,
})

Expand Down
9 changes: 2 additions & 7 deletions homeassistant/components/camera/proxy.py
Expand Up @@ -11,13 +11,11 @@
import voluptuous as vol

from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \
HTTP_HEADER_HA_AUTH
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.components.camera import async_get_still_stream

REQUIREMENTS = ['pillow==5.4.1']

Expand Down Expand Up @@ -209,9 +207,6 @@ def __init__(self, hass, config):
or config.get(CONF_CACHE_IMAGES))
self._last_image_time = dt_util.utc_from_timestamp(0)
self._last_image = None
self._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
if self.hass.config.api.api_password is not None else None)
self._mode = config.get(CONF_MODE)

def camera_image(self):
Expand Down Expand Up @@ -252,7 +247,7 @@ async def handle_async_mjpeg_stream(self, request):
return await self.hass.components.camera.async_get_mjpeg_stream(
request, self._proxied_camera)

return await async_get_still_stream(
return await self.hass.components.camera.async_get_still_stream(
request, self._async_stream_image,
self.content_type, self.frame_interval)

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/__init__.py
Expand Up @@ -407,7 +407,7 @@ async def get(self, request, extra=None):
})

no_auth = '1'
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
if not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'
Copy link
Member

Choose a reason for hiding this comment

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

For a future PR, this is no longer used.


Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/hassio/auth.py
Expand Up @@ -57,9 +57,9 @@ async def post(self, request, data):

def _get_provider(self):
"""Return Homeassistant auth provider."""
for prv in self.hass.auth.auth_providers:
if prv.type == 'homeassistant':
return prv
prv = self.hass.auth.get_auth_provider('homeassistant', None)
if prv is not None:
return prv

_LOGGER.error("Can't find Home Assistant auth.")
raise HTTPNotFound()
Expand Down
7 changes: 4 additions & 3 deletions homeassistant/components/hassio/handler.py
Expand Up @@ -7,8 +7,10 @@
import async_timeout

from homeassistant.components.http import (
CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE)
CONF_SERVER_HOST,
CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE,
)
from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT

from .const import X_HASSIO
Expand Down Expand Up @@ -125,7 +127,6 @@ async def update_hass_api(self, http_config, refresh_token):
options = {
'ssl': CONF_SSL_CERTIFICATE in http_config,
'port': port,
'password': http_config.get(CONF_API_PASSWORD),
awarecan marked this conversation as resolved.
Show resolved Hide resolved
'watchdog': True,
'refresh_token': refresh_token,
}
Expand Down
58 changes: 27 additions & 31 deletions homeassistant/components/http/__init__.py
Expand Up @@ -18,7 +18,12 @@

from .auth import setup_auth
from .ban import setup_bans
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
from .const import ( # noqa
KEY_AUTHENTICATED,
KEY_HASS,
KEY_HASS_USER,
KEY_REAL_IP,
)
from .cors import setup_cors
from .real_ip import setup_real_ip
from .static import CACHE_HEADERS, CachingStaticResource
Expand Down Expand Up @@ -66,8 +71,22 @@ def trusted_networks_deprecated(value):
return value


def api_password_deprecated(value):
"""Warn user api_password config is deprecated."""
if not value:
return value

_LOGGER.warning(
"Configuring api_password via the http component has been"
" deprecated. Use the legacy api password auth provider instead."
" For instructions, see https://www.home-assistant.io/docs/"
"authentication/providers/#legacy-api-password")
return value


HTTP_SCHEMA = vol.Schema({
vol.Optional(CONF_API_PASSWORD): cv.string,
vol.Optional(CONF_API_PASSWORD):
vol.All(cv.string, api_password_deprecated),
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_BASE_URL): cv.string,
Expand Down Expand Up @@ -98,12 +117,10 @@ class ApiConfig:
"""Configuration settings for API server."""

def __init__(self, host: str, port: Optional[int] = SERVER_PORT,
use_ssl: bool = False,
api_password: Optional[str] = None) -> None:
use_ssl: bool = False) -> None:
"""Initialize a new API config object."""
self.host = host
self.port = port
self.api_password = api_password

host = host.rstrip('/')
if host.startswith(("http://", "https://")):
Expand Down Expand Up @@ -133,7 +150,6 @@ async def async_setup(hass, config):
cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
ssl_profile = conf[CONF_SSL_PROFILE]
Expand All @@ -146,14 +162,12 @@ async def async_setup(hass, config):
hass,
server_host=server_host,
server_port=server_port,
api_password=api_password,
ssl_certificate=ssl_certificate,
ssl_peer_certificate=ssl_peer_certificate,
ssl_key=ssl_key,
cors_origins=cors_origins,
use_x_forwarded_for=use_x_forwarded_for,
trusted_proxies=trusted_proxies,
trusted_networks=trusted_networks,
login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled,
ssl_profile=ssl_profile,
Expand Down Expand Up @@ -183,57 +197,39 @@ async def start_server(event):
host = hass_util.get_local_ip()
port = server_port

hass.config.api = ApiConfig(host, port, ssl_certificate is not None,
api_password)
hass.config.api = ApiConfig(host, port, ssl_certificate is not None)

return True


class HomeAssistantHTTP:
"""HTTP server for Home Assistant."""

def __init__(self, hass, api_password,
def __init__(self, hass,
ssl_certificate, ssl_peer_certificate,
ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_proxies, trusted_networks,
use_x_forwarded_for, trusted_proxies,
login_threshold, is_ban_enabled, ssl_profile):
"""Initialize the HTTP Home Assistant server."""
app = self.app = web.Application(middlewares=[])
app[KEY_HASS] = hass

# This order matters
setup_real_ip(app, use_x_forwarded_for, trusted_proxies)

if is_ban_enabled:
setup_bans(hass, app, login_threshold)

if hass.auth.support_legacy:
_LOGGER.warning(
"legacy_api_password support has been enabled. If you don't "
"require it, remove the 'api_password' from your http config.")

for prv in hass.auth.auth_providers:
if prv.type == 'trusted_networks':
# auth_provider.trusted_networks will override
# http.trusted_networks, http.trusted_networks will be
# removed from future release
trusted_networks = prv.trusted_networks
break

setup_auth(app, trusted_networks,
api_password if hass.auth.support_legacy else None)
setup_auth(hass, app)

setup_cors(app, cors_origins)

app['hass'] = hass

self.hass = hass
self.api_password = api_password
self.ssl_certificate = ssl_certificate
self.ssl_peer_certificate = ssl_peer_certificate
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.trusted_networks = trusted_networks
self.is_ban_enabled = is_ban_enabled
self.ssl_profile = ssl_profile
self._handler = None
Expand Down