Use HA native OAuth2 flow for google assistant components (#16848)
* Use HA native OAuth2 flow for google assistant components * Lint * Force breaking changes * Fix CONFIG_SCHEMA
This commit is contained in:
parent
3cba2e695c
commit
92a5068977
6 changed files with 43 additions and 162 deletions
|
@ -14,20 +14,18 @@ import async_timeout
|
|||
import voluptuous as vol
|
||||
|
||||
# Typing imports
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
|
||||
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
|
||||
DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY,
|
||||
DOMAIN, CONF_PROJECT_ID, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT,
|
||||
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY,
|
||||
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG,
|
||||
CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT
|
||||
)
|
||||
from .auth import GoogleAssistantAuthView
|
||||
from .http import async_register_http
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -43,34 +41,28 @@ ENTITY_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_ROOM_HINT): cv.string
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: {
|
||||
vol.Required(CONF_PROJECT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT,
|
||||
default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS,
|
||||
default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_AGENT_USER_ID,
|
||||
default=DEFAULT_AGENT_USER_ID): cv.string,
|
||||
vol.Optional(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}
|
||||
}
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
GOOGLE_ASSISTANT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PROJECT_ID): cv.string,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT,
|
||||
default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS,
|
||||
default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: GOOGLE_ASSISTANT_SCHEMA
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
||||
"""Activate Google Actions component."""
|
||||
config = yaml_config.get(DOMAIN, {})
|
||||
agent_user_id = config.get(CONF_AGENT_USER_ID)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
hass.http.register_view(GoogleAssistantAuthView(hass, config))
|
||||
async_register_http(hass, config)
|
||||
|
||||
async def request_sync_service_handler(call):
|
||||
async def request_sync_service_handler(call: ServiceCall):
|
||||
"""Handle request sync service calls."""
|
||||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
|
@ -78,7 +70,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
|||
res = await websession.post(
|
||||
REQUEST_SYNC_BASE_URL,
|
||||
params={'key': api_key},
|
||||
json={'agent_user_id': agent_user_id})
|
||||
json={'agent_user_id': call.context.user_id})
|
||||
_LOGGER.info("Submitted request_sync request to Google")
|
||||
res.raise_for_status()
|
||||
except aiohttp.ClientResponseError:
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
"""Google Assistant OAuth View."""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
# Typing imports
|
||||
# if False:
|
||||
from aiohttp.web import Request, Response
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
HTTP_BAD_REQUEST,
|
||||
HTTP_UNAUTHORIZED,
|
||||
HTTP_MOVED_PERMANENTLY,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN
|
||||
)
|
||||
|
||||
BASE_OAUTH_URL = 'https://oauth-redirect.googleusercontent.com'
|
||||
REDIRECT_TEMPLATE_URL = \
|
||||
'{}/r/{}#access_token={}&token_type=bearer&state={}'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GoogleAssistantAuthView(HomeAssistantView):
|
||||
"""Handle Google Actions auth requests."""
|
||||
|
||||
url = GOOGLE_ASSISTANT_API_ENDPOINT + '/auth'
|
||||
name = 'api:google_assistant:auth'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None:
|
||||
"""Initialize instance of the view."""
|
||||
super().__init__()
|
||||
|
||||
self.project_id = cfg.get(CONF_PROJECT_ID)
|
||||
self.client_id = cfg.get(CONF_CLIENT_ID)
|
||||
self.access_token = cfg.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
async def get(self, request: Request) -> Response:
|
||||
"""Handle oauth token request."""
|
||||
query = request.query
|
||||
redirect_uri = query.get('redirect_uri')
|
||||
if not redirect_uri:
|
||||
msg = 'missing redirect_uri field'
|
||||
_LOGGER.warning(msg)
|
||||
return self.json_message(msg, status_code=HTTP_BAD_REQUEST)
|
||||
|
||||
if self.project_id not in redirect_uri:
|
||||
msg = 'missing project_id in redirect_uri'
|
||||
_LOGGER.warning(msg)
|
||||
return self.json_message(msg, status_code=HTTP_BAD_REQUEST)
|
||||
|
||||
state = query.get('state')
|
||||
if not state:
|
||||
msg = 'oauth request missing state'
|
||||
_LOGGER.warning(msg)
|
||||
return self.json_message(msg, status_code=HTTP_BAD_REQUEST)
|
||||
|
||||
client_id = query.get('client_id')
|
||||
if self.client_id != client_id:
|
||||
msg = 'invalid client id'
|
||||
_LOGGER.warning(msg)
|
||||
return self.json_message(msg, status_code=HTTP_UNAUTHORIZED)
|
||||
|
||||
generated_url = redirect_url(self.project_id, self.access_token, state)
|
||||
|
||||
_LOGGER.info('user login in from Google Assistant')
|
||||
return self.json_message(
|
||||
'redirect success',
|
||||
status_code=HTTP_MOVED_PERMANENTLY,
|
||||
headers={'Location': generated_url})
|
||||
|
||||
|
||||
def redirect_url(project_id: str, access_token: str, state: str) -> str:
|
||||
"""Generate the redirect format for the oauth request."""
|
||||
return REDIRECT_TEMPLATE_URL.format(BASE_OAUTH_URL, project_id,
|
||||
access_token, state)
|
|
@ -8,10 +8,7 @@ CONF_ENTITY_CONFIG = 'entity_config'
|
|||
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
|
||||
CONF_EXPOSED_DOMAINS = 'exposed_domains'
|
||||
CONF_PROJECT_ID = 'project_id'
|
||||
CONF_ACCESS_TOKEN = 'access_token'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_ALIASES = 'aliases'
|
||||
CONF_AGENT_USER_ID = 'agent_user_id'
|
||||
CONF_API_KEY = 'api_key'
|
||||
CONF_ROOM_HINT = 'room'
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ https://home-assistant.io/components/google_assistant/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
from aiohttp.web import Request, Response
|
||||
|
||||
# Typing imports
|
||||
|
@ -15,10 +14,8 @@ from homeassistant.core import callback
|
|||
|
||||
from .const import (
|
||||
GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_EXPOSE_BY_DEFAULT,
|
||||
CONF_EXPOSED_DOMAINS,
|
||||
CONF_AGENT_USER_ID,
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_EXPOSE,
|
||||
)
|
||||
|
@ -31,10 +28,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||
@callback
|
||||
def async_register_http(hass, cfg):
|
||||
"""Register HTTP views for Google Assistant."""
|
||||
access_token = cfg.get(CONF_ACCESS_TOKEN)
|
||||
expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT)
|
||||
exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS)
|
||||
agent_user_id = cfg.get(CONF_AGENT_USER_ID)
|
||||
entity_config = cfg.get(CONF_ENTITY_CONFIG) or {}
|
||||
|
||||
def is_exposed(entity) -> bool:
|
||||
|
@ -57,9 +52,8 @@ def async_register_http(hass, cfg):
|
|||
|
||||
return is_default_exposed or explicit_expose
|
||||
|
||||
gass_config = Config(is_exposed, agent_user_id, entity_config)
|
||||
hass.http.register_view(
|
||||
GoogleAssistantView(access_token, gass_config))
|
||||
GoogleAssistantView(is_exposed, entity_config))
|
||||
|
||||
|
||||
class GoogleAssistantView(HomeAssistantView):
|
||||
|
@ -67,20 +61,19 @@ class GoogleAssistantView(HomeAssistantView):
|
|||
|
||||
url = GOOGLE_ASSISTANT_API_ENDPOINT
|
||||
name = 'api:google_assistant'
|
||||
requires_auth = False # Uses access token from oauth flow
|
||||
requires_auth = True
|
||||
|
||||
def __init__(self, access_token, gass_config):
|
||||
def __init__(self, is_exposed, entity_config):
|
||||
"""Initialize the Google Assistant request handler."""
|
||||
self.access_token = access_token
|
||||
self.gass_config = gass_config
|
||||
self.is_exposed = is_exposed
|
||||
self.entity_config = entity_config
|
||||
|
||||
async def post(self, request: Request) -> Response:
|
||||
"""Handle Google Assistant requests."""
|
||||
auth = request.headers.get(AUTHORIZATION, None)
|
||||
if 'Bearer {}'.format(self.access_token) != auth:
|
||||
return self.json_message("missing authorization", status_code=401)
|
||||
|
||||
message = await request.json() # type: dict
|
||||
config = Config(self.is_exposed,
|
||||
request['hass_user'].id,
|
||||
self.entity_config)
|
||||
result = await async_handle_message(
|
||||
request.app['hass'], self.gass_config, message)
|
||||
request.app['hass'], config, message)
|
||||
return self.json(result)
|
||||
|
|
|
@ -23,7 +23,12 @@ HA_HEADERS = {
|
|||
PROJECT_ID = 'hasstest-1234'
|
||||
CLIENT_ID = 'helloworld'
|
||||
ACCESS_TOKEN = 'superdoublesecret'
|
||||
AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_header(hass_access_token):
|
||||
"""Generate an HTTP header with bearer token authorization."""
|
||||
return {AUTHORIZATION: 'Bearer {}'.format(hass_access_token)}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -33,8 +38,6 @@ def assistant_client(loop, hass, aiohttp_client):
|
|||
setup.async_setup_component(hass, 'google_assistant', {
|
||||
'google_assistant': {
|
||||
'project_id': PROJECT_ID,
|
||||
'client_id': CLIENT_ID,
|
||||
'access_token': ACCESS_TOKEN,
|
||||
'entity_config': {
|
||||
'light.ceiling_lights': {
|
||||
'aliases': ['top lights', 'ceiling lights'],
|
||||
|
@ -97,31 +100,14 @@ def hass_fixture(loop, hass):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_auth(assistant_client):
|
||||
"""Test the auth process."""
|
||||
result = yield from assistant_client.get(
|
||||
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT + '/auth',
|
||||
params={
|
||||
'redirect_uri':
|
||||
'http://testurl/r/{}'.format(PROJECT_ID),
|
||||
'client_id': CLIENT_ID,
|
||||
'state': 'random1234',
|
||||
},
|
||||
allow_redirects=False)
|
||||
assert result.status == 301
|
||||
loc = result.headers.get('Location')
|
||||
assert ACCESS_TOKEN in loc
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_sync_request(hass_fixture, assistant_client):
|
||||
def test_sync_request(hass_fixture, assistant_client, auth_header):
|
||||
"""Test a sync request."""
|
||||
reqid = '5711642932632160983'
|
||||
data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]}
|
||||
result = yield from assistant_client.post(
|
||||
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
data=json.dumps(data),
|
||||
headers=AUTH_HEADER)
|
||||
headers=auth_header)
|
||||
assert result.status == 200
|
||||
body = yield from result.json()
|
||||
assert body.get('requestId') == reqid
|
||||
|
@ -141,7 +127,7 @@ def test_sync_request(hass_fixture, assistant_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_query_request(hass_fixture, assistant_client):
|
||||
def test_query_request(hass_fixture, assistant_client, auth_header):
|
||||
"""Test a query request."""
|
||||
reqid = '5711642932632160984'
|
||||
data = {
|
||||
|
@ -165,7 +151,7 @@ def test_query_request(hass_fixture, assistant_client):
|
|||
result = yield from assistant_client.post(
|
||||
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
data=json.dumps(data),
|
||||
headers=AUTH_HEADER)
|
||||
headers=auth_header)
|
||||
assert result.status == 200
|
||||
body = yield from result.json()
|
||||
assert body.get('requestId') == reqid
|
||||
|
@ -180,7 +166,7 @@ def test_query_request(hass_fixture, assistant_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_query_climate_request(hass_fixture, assistant_client):
|
||||
def test_query_climate_request(hass_fixture, assistant_client, auth_header):
|
||||
"""Test a query request."""
|
||||
reqid = '5711642932632160984'
|
||||
data = {
|
||||
|
@ -200,7 +186,7 @@ def test_query_climate_request(hass_fixture, assistant_client):
|
|||
result = yield from assistant_client.post(
|
||||
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
data=json.dumps(data),
|
||||
headers=AUTH_HEADER)
|
||||
headers=auth_header)
|
||||
assert result.status == 200
|
||||
body = yield from result.json()
|
||||
assert body.get('requestId') == reqid
|
||||
|
@ -229,7 +215,7 @@ def test_query_climate_request(hass_fixture, assistant_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_query_climate_request_f(hass_fixture, assistant_client):
|
||||
def test_query_climate_request_f(hass_fixture, assistant_client, auth_header):
|
||||
"""Test a query request."""
|
||||
# Mock demo devices as fahrenheit to see if we convert to celsius
|
||||
hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT
|
||||
|
@ -256,7 +242,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client):
|
|||
result = yield from assistant_client.post(
|
||||
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
data=json.dumps(data),
|
||||
headers=AUTH_HEADER)
|
||||
headers=auth_header)
|
||||
assert result.status == 200
|
||||
body = yield from result.json()
|
||||
assert body.get('requestId') == reqid
|
||||
|
@ -286,7 +272,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_execute_request(hass_fixture, assistant_client):
|
||||
def test_execute_request(hass_fixture, assistant_client, auth_header):
|
||||
"""Test an execute request."""
|
||||
reqid = '5711642932632160985'
|
||||
data = {
|
||||
|
@ -358,7 +344,7 @@ def test_execute_request(hass_fixture, assistant_client):
|
|||
result = yield from assistant_client.post(
|
||||
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
data=json.dumps(data),
|
||||
headers=AUTH_HEADER)
|
||||
headers=auth_header)
|
||||
assert result.status == 200
|
||||
body = yield from result.json()
|
||||
assert body.get('requestId') == reqid
|
||||
|
|
|
@ -5,7 +5,6 @@ from homeassistant.setup import async_setup_component
|
|||
from homeassistant.components import google_assistant as ga
|
||||
|
||||
GA_API_KEY = "Agdgjsj399sdfkosd932ksd"
|
||||
GA_AGENT_USER_ID = "testid"
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -17,9 +16,6 @@ def test_request_sync_service(aioclient_mock, hass):
|
|||
yield from async_setup_component(hass, 'google_assistant', {
|
||||
'google_assistant': {
|
||||
'project_id': 'test_project',
|
||||
'client_id': 'r7328kwdsdfsdf03223409',
|
||||
'access_token': '8wdsfjsf932492342349234',
|
||||
'agent_user_id': GA_AGENT_USER_ID,
|
||||
'api_key': GA_API_KEY
|
||||
}})
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue