- 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 devicepull/75/head
parent
38494522de
commit
8484d8289b
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue