- Changing the validation in the endpoints used to update the device fields and to activate the device and to upload the skill settings

- Checking the authentication token in the before_request function. This is a tricky to make the api compatible with the mycroft core is expecting today. Today tartarus returns 401 when it calls and endpoint with a non existent path (like /device/{uuid} without pass the uuid). That is the value core expect to check if it needs to perform a device pairing. We should change that in future versions
- Changed device endpoint to send the user uuid together with the device
pull/75/head
Matheus Lima 2019-03-08 17:09:53 -03:00
parent 38494522de
commit 8484d8289b
10 changed files with 77 additions and 37 deletions

View File

@ -4,6 +4,7 @@ from flask import Flask
from selene.api import SeleneResponse, selene_api
from selene.api.base_config import get_base_config
from selene.api.public_endpoint import check_oauth_token
from selene.util.cache import SeleneCache
from .endpoints.account_device import AccountDeviceEndpoint
from .endpoints.device import DeviceEndpoint
@ -120,3 +121,11 @@ public.add_url_rule(
view_func=WolframAlphaSpokenEndpoint.as_view('wolfram_alpha_spoken_api'),
methods=['GET']
)
"""
This is a workaround to allow the API return 401 when we call a non existent path. Use case:
GET /device/{uuid} with empty uuid. Core today uses the 401 to validate if it needs to perform a pairing process
Whe should fix that in a future version because we have to return 404 when we call a non existent path
"""
public.before_request(check_oauth_token)

View File

@ -1,5 +1,4 @@
import json
from dataclasses import asdict
from http import HTTPStatus
from schematics import Model
@ -11,10 +10,10 @@ from selene.util.db import get_db_connection
class UpdateDevice(Model):
coreVersion = StringType()
platform = StringType()
coreVersion = StringType(default='unknown')
platform = StringType(default='unknown')
platform_build = StringType()
enclosureVersion = StringType()
enclosureVersion = StringType(default='unknown')
class DeviceEndpoint(PublicEndpoint):
@ -27,13 +26,14 @@ class DeviceEndpoint(PublicEndpoint):
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
device = DeviceRepository(db).get_device_by_id(device_id)
if device:
device = asdict(device)
if 'placement' in device:
device['description'] = device.pop('placement')
if 'core_version' in device:
device['coreVersion'] = device.pop('core_version')
if 'enclosure_version' in device:
device['enclosureVersion'] = device.pop('enclosure_version')
device['user'] = dict(uuid=device['account_id'])
del device['account_id']
response = device, HTTPStatus.OK
else:
response = '', HTTPStatus.NO_CONTENT
@ -44,11 +44,17 @@ class DeviceEndpoint(PublicEndpoint):
payload = json.loads(self.request.data)
update_device = UpdateDevice(payload)
update_device.validate()
platform = payload.get('platform')
platform = 'unknown' if platform is None else platform
enclosure_version = payload.get('enclosureVersion')
enclosure_version = 'unknown' if enclosure_version is None else enclosure_version
core_version = payload.get('coreVersion')
core_version = 'unknown' if core_version is None else core_version
with get_db_connection(self.config['DB_CONNECTION_POOL']) as db:
DeviceRepository(db).update_device(
device_id,
payload.get('platform'),
payload.get('enclosureVersion'),
payload.get('coreVersion')
platform,
enclosure_version,
core_version
)
return '', HTTPStatus.OK

View File

@ -14,8 +14,9 @@ class DeviceActivate(Model):
token = StringType(required=True)
state = StringType(required=True)
platform = StringType(default='unknown')
core_version = StringType(default='unknown')
enclosure_version = StringType(default='unknown')
coreVersion = StringType(default='unknown')
enclosureVersion = StringType(default='unknown')
platform_build = StringType()
class DeviceActivateEndpoint(PublicEndpoint):
@ -27,16 +28,14 @@ class DeviceActivateEndpoint(PublicEndpoint):
def post(self):
payload = json.loads(self.request.data)
device_activate = DeviceActivate(payload)
if device_activate:
pairing = self._get_pairing_session(device_activate)
if pairing:
device_id = pairing['uuid']
self._activate(device_id, device_activate)
response = generate_device_login(device_id, self.cache), HTTPStatus.OK
else:
response = '', HTTPStatus.NO_CONTENT
device_activate.validate()
pairing = self._get_pairing_session(device_activate)
if pairing:
device_id = pairing['uuid']
self._activate(device_id, device_activate)
response = generate_device_login(device_id, self.cache), HTTPStatus.OK
else:
response = '', HTTPStatus.NO_CONTENT
response = '', HTTPStatus.NOT_FOUND
return response
def _get_pairing_session(self, device_activate: DeviceActivate):
@ -56,8 +55,8 @@ class DeviceActivateEndpoint(PublicEndpoint):
DeviceRepository(db).update_device(
device_id,
str(device_activate.platform),
str(device_activate.enclosure_version),
str(device_activate.core_version)
str(device_activate.enclosureVersion),
str(device_activate.coreVersion)
)
@staticmethod

View File

@ -3,6 +3,7 @@ import json
from http import HTTPStatus
from selene.api import PublicEndpoint, generate_device_login
from selene.util.auth import AuthenticationError
class DeviceRefreshTokenEndpoint(PublicEndpoint):
@ -14,11 +15,17 @@ class DeviceRefreshTokenEndpoint(PublicEndpoint):
self.sha512 = hashlib.sha512()
def get(self):
self._authenticate()
refresh = self.request.headers['Authorization'][len('Bearer '):]
session = self._refresh_session_token(refresh)
if session:
response = session, HTTPStatus.OK
headers = self.request.headers
if 'Authorization' not in headers:
raise AuthenticationError('Oauth token not found')
token_header = self.request.headers['Authorization']
if token_header.startswith('Bearer '):
refresh = token_header[len('Bearer '):]
session = self._refresh_session_token(refresh)
if session:
response = session, HTTPStatus.OK
else:
response = '', HTTPStatus.UNAUTHORIZED
else:
response = '', HTTPStatus.UNAUTHORIZED
return response

View File

@ -17,7 +17,7 @@ class SkillField(Model):
placeholder = StringType()
hide = BooleanType()
value = StringType()
option = StringType()
options = StringType()
class SkillSection(Model):
@ -33,7 +33,7 @@ class Skill(Model):
name = StringType(required=True)
identifier = StringType(required=True)
skillMetadata = ModelType(SkillMetadata)
color = StringType()
class DeviceSkillsEndpoint(PublicEndpoint):
"""Fetch all skills associated with a given device using the API v1 format"""

View File

@ -35,8 +35,8 @@ def activate_device(context):
'token': context.pairing['token'],
'state': context.pairing['state'],
'platform': 'picroft',
'core_version': '18.8.0',
'enclosure_version': '1.4.0'
'coreVersion': '18.8.0',
'enclosureVersion': '1.4.0'
}
response = context.client.post('/v1/device/activate', data=json.dumps(activate), content_type='application_json')
context.activate_device_response = response

View File

@ -33,6 +33,9 @@ def validate_response(context):
assert_that(device, has_key('coreVersion'))
assert_that(device, has_key('enclosureVersion'))
assert_that(device, has_key('platform'))
assert_that(device, has_key('user'))
assert_that(device['user'], has_key('uuid'))
assert_that(device['user']['uuid'], equal_to(context.account.id))
@when('try to fetch a device without the authorization header')

View File

@ -11,6 +11,25 @@ from ..util.cache import SeleneCache
ONE_DAY = 86400
def check_oauth_token():
exclude_paths = ['/v1/device/code', '/v1/device/activate', '/api/account', '/v1/auth/token']
exclude = any(request.path.startswith(path) for path in exclude_paths)
if not exclude:
headers = request.headers
if 'Authorization' not in headers:
raise AuthenticationError('Oauth token not found')
token_header = headers['Authorization']
device_authenticated = False
if token_header.startswith('Bearer '):
token = token_header[len('Bearer '):]
session = current_app.config['SELENE_CACHE'].get('device.token.access:{access}'.format(access=token))
if session:
device_authenticated = True
if not device_authenticated:
raise AuthenticationError('device not authorized')
def generate_device_login(device_id: str, cache: SeleneCache) -> dict:
"""Generates a login session for a given device id"""
sha512 = hashlib.sha512()
@ -49,7 +68,7 @@ class PublicEndpoint(MethodView):
if 'Authorization' not in headers:
raise AuthenticationError('Oauth token not found')
token_header = self.request.headers['Authorization']
device_authenticated = True
device_authenticated = False
if token_header.startswith('Bearer '):
token = token_header[len('Bearer '):]
session = self.cache.get('device.token.access:{access}'.format(access=token))

View File

@ -14,7 +14,7 @@ class DeviceRepository(object):
def __init__(self, db):
self.cursor = Cursor(db)
def get_device_by_id(self, device_id: str) -> Device:
def get_device_by_id(self, device_id: str) -> dict:
"""Fetch a device using a given device id
:param device_id: uuid
@ -25,9 +25,7 @@ class DeviceRepository(object):
args=dict(device_id=device_id)
)
sql_results = self.cursor.select_one(query)
if sql_results:
return Device(**sql_results)
return self.cursor.select_one(query)
def get_devices_by_account_id(self, account_id: str) -> List[Device]:
"""Fetch all devices associated to a user from a given account id

View File

@ -5,8 +5,7 @@ SELECT
dev.enclosure_version,
dev.core_version,
dev.placement,
json_build_object('id', wk_word.id, 'wake_word', wk_word.wake_word, 'engine', wk_word.engine) as wake_word,
json_build_object('id', tts.id, 'setting_name', tts.setting_name, 'display_name', tts.display_name, 'engine', tts.engine) as text_to_speech
dev.account_id
FROM device.device dev
INNER JOIN
device.wake_word wk_word ON dev.wake_word_id = wk_word.id