selene-backend/shared/selene/api/public_endpoint.py

136 lines
5.1 KiB
Python

import hashlib
import json
import uuid
from flask import (
current_app,
request,
Response,
after_this_request
)
from flask import g as global_context
from flask.views import MethodView
from selene.api.etag import ETagManager
from selene.util.auth import AuthenticationError
from selene.util.db import connect_to_db
from selene.util.not_modified import NotModifiedError
from ..util.cache import SeleneCache
ONE_DAY = 86400
def check_oauth_token():
global_context.url = request.url
exclude_paths = ['/v1/device/code', '/v1/device/activate', '/api/account', '/v1/auth/token', '/v1/auth/callback']
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()
sha512.update(bytes(str(uuid.uuid4()), 'utf-8'))
access = sha512.hexdigest()
sha512.update(bytes(str(uuid.uuid4()), 'utf-8'))
refresh = sha512.hexdigest()
login = dict(
uuid=device_id,
accessToken=access,
refreshToken=refresh,
expiration=ONE_DAY
)
login_json = json.dumps(login)
# Storing device access token for one:
cache.set_with_expiration(
'device.token.access:{access}'.format(access=access),
login_json,
ONE_DAY
)
# Storing device refresh token for ever:
cache.set('device.token.refresh:{refresh}'.format(refresh=refresh), login_json)
# Storing the login session by uuid (that allows us to delete session using the uuid)
cache.set('device.session:{uuid}'.format(uuid=device_id), login_json)
return login
def delete_device_login(device_id: str, cache: SeleneCache):
session = cache.get('device.session:{uuid}'.format(uuid=device_id))
if session is not None:
session = json.loads(session)
access_token = session['accessToken']
cache.delete('device.token.access:{access}'.format(access=access_token))
refresh_token = session['refreshToken']
cache.delete('device.refresh.token:{refresh}'.format(refresh=refresh_token))
cache.delete('device.session:{uuid}'.format(uuid=device_id))
class PublicEndpoint(MethodView):
"""Abstract class for all endpoints used by Mycroft devices"""
def __init__(self):
self.config: dict = current_app.config
self.request = request
global_context.url = request.url
self.cache: SeleneCache = self.config['SELENE_CACHE']
self.etag_manager: ETagManager = ETagManager(self.cache, self.config)
@property
def db(self):
if 'db' not in global_context:
global_context.db = connect_to_db(
current_app.config['DB_CONNECTION_CONFIG']
)
return global_context.db
def _authenticate(self, device_id: str = None):
headers = self.request.headers
if 'Authorization' not in headers:
raise AuthenticationError('Oauth token not found')
token_header = self.request.headers['Authorization']
device_authenticated = False
if token_header.startswith('Bearer '):
token = token_header[len('Bearer '):]
session = self.cache.get('device.token.access:{access}'.format(access=token))
if session is not None:
session = json.loads(session)
device_uuid = session['uuid']
global_context.device_id = device_uuid
if device_id is not None:
device_authenticated = (device_id == device_uuid)
else:
device_authenticated = True
if not device_authenticated:
raise AuthenticationError('device not authorized')
def _add_etag(self, key):
"""Add a etag header to the response. We try to get the etag from the cache using the given key.
If the cache has the etag, we use it, otherwise we generate a etag, store it and add it to the response"""
etag = self.etag_manager.get(key)
@after_this_request
def set_etag_header(response: Response):
response.headers['ETag'] = etag
return response
def _validate_etag(self, key):
etag_from_request = self.request.headers.get('If-None-Match')
if etag_from_request is not None:
etag_from_cache = self.cache.get(key)
if etag_from_cache is not None and etag_from_request == etag_from_cache.decode('utf-8'):
raise NotModifiedError()