forked from shaba/openuds
fixes for python 3.7 on REST
This commit is contained in:
parent
f464d78f99
commit
79f41b3e1a
@ -29,8 +29,12 @@
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import os.path
|
||||
import pkgutil
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from django import http
|
||||
from django.views.generic.base import View
|
||||
@ -51,7 +55,6 @@ from .handlers import (
|
||||
|
||||
from . import processors
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [str(v) for v in ['Handler', 'Dispatcher']]
|
||||
@ -63,64 +66,62 @@ class Dispatcher(View):
|
||||
"""
|
||||
This class is responsible of dispatching REST requests
|
||||
"""
|
||||
# This attribute will contain all paths-->handler relations, added at Initialized method
|
||||
services = {'': None} # Will include a default /rest handler, but rigth now this will be fine
|
||||
# This attribute will contain all paths--> handler relations, filled at Initialized method
|
||||
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {'': None} # Will include a default /rest handler, but rigth now this will be fine
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
def dispatch(self, request: http.HttpRequest, *args, **kwargs):
|
||||
"""
|
||||
Processes the REST request and routes it wherever it needs to be routed
|
||||
"""
|
||||
|
||||
# Remove session, so response middleware do nothing with this
|
||||
# Remove session from request, so response middleware do nothing with this
|
||||
del request.session
|
||||
|
||||
# Now we extract method and possible variables from path
|
||||
path = kwargs['arguments'].split('/')
|
||||
path: typing.List[str] = kwargs['arguments'].split('/')
|
||||
del kwargs['arguments']
|
||||
|
||||
# Transverse service nodes too look for path
|
||||
# Transverse service nodes, so we can locate class processing this path
|
||||
service = Dispatcher.services
|
||||
full_path = []
|
||||
content_type = None
|
||||
full_path_lst: typing.List[str] = []
|
||||
# Guess content type from content type header (post) or ".xxx" to method
|
||||
content_type: str = request.META.get('CONTENT_TYPE', 'json')
|
||||
|
||||
cls = None
|
||||
while len(path) > 0:
|
||||
# .json, .xml, ... will break path recursion
|
||||
while path:
|
||||
# .json, .xml, .anything will break path recursion
|
||||
if path[0].find('.') != -1:
|
||||
content_type = path[0].split('.')[1]
|
||||
|
||||
clean_path = path[0].split('.')[0]
|
||||
if clean_path in service:
|
||||
service = service[clean_path]
|
||||
full_path.append(path[0])
|
||||
full_path_lst.append(path[0])
|
||||
path = path[1:]
|
||||
else:
|
||||
break
|
||||
|
||||
full_path = '/'.join(full_path)
|
||||
full_path = '/'.join(full_path_lst)
|
||||
logger.debug("REST request: %s (%s)", full_path, content_type)
|
||||
|
||||
# Here, service points to the path
|
||||
cls = service['']
|
||||
cls: typing.Optional[typing.Type[Handler]] = service['']
|
||||
if cls is None:
|
||||
return http.HttpResponseNotFound('method not found', content_type="text/plain")
|
||||
return http.HttpResponseNotFound('Method not found', content_type="text/plain")
|
||||
|
||||
# Guess content type from content type header (post) or ".xxx" to method
|
||||
try:
|
||||
processor = processors.available_processors_ext_dict[content_type](request)
|
||||
except Exception:
|
||||
processor = processors.available_processors_mime_dict.get(request.META.get('CONTENT_TYPE', 'json'), processors.default_processor)(request)
|
||||
processor = processors.available_processors_ext_dict.get(content_type, processors.default_processor)(request)
|
||||
|
||||
# Obtain method to be invoked
|
||||
http_method = request.method.lower()
|
||||
http_method: str = request.method.lower()
|
||||
|
||||
args = path
|
||||
# Path here has "remaining" path, that is, method part has been removed
|
||||
args = tuple(path)
|
||||
|
||||
handler = None
|
||||
|
||||
try:
|
||||
handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs)
|
||||
operation = getattr(handler, http_method)
|
||||
operation: typing.Callable[[], typing.Any] = getattr(handler, http_method)
|
||||
except processors.ParametersException as e:
|
||||
logger.debug('Path: %s', full_path)
|
||||
logger.debug('Error: %s', e)
|
||||
@ -144,6 +145,7 @@ class Dispatcher(View):
|
||||
|
||||
if not handler.raw: # Raw handlers will return an HttpResponse Object
|
||||
response = processor.getResponse(response)
|
||||
# Set response headers
|
||||
for k, val in handler.headers().items():
|
||||
response[k] = val
|
||||
return response
|
||||
@ -164,25 +166,24 @@ class Dispatcher(View):
|
||||
return http.HttpResponseServerError(str(e), content_type="text/plain")
|
||||
|
||||
@staticmethod
|
||||
def registerSubclasses(classes):
|
||||
def registerSubclasses(classes: typing.List[typing.Type[Handler]]):
|
||||
"""
|
||||
Try to register Handler subclasses that have not been inherited
|
||||
"""
|
||||
for cls in classes:
|
||||
if len(cls.__subclasses__()) == 0: # Only classes that has not been inherited will be registered as Handlers
|
||||
logger.debug('Found class %s', cls)
|
||||
if cls.name is None:
|
||||
if not cls.__subclasses__(): # Only classes that has not been inherited will be registered as Handlers
|
||||
if not cls.name:
|
||||
name = cls.__name__.lower()
|
||||
else:
|
||||
name = cls.name
|
||||
logger.debug('Adding handler %s for method %s in path %s', cls, name, cls.path)
|
||||
service_node = Dispatcher.services
|
||||
if cls.path is not None:
|
||||
service_node = Dispatcher.services # Root path
|
||||
if cls.path:
|
||||
for k in cls.path.split('/'):
|
||||
if service_node.get(k) is None:
|
||||
if k not in service_node:
|
||||
service_node[k] = {'': None}
|
||||
service_node = service_node[k]
|
||||
if service_node.get(name) is None:
|
||||
if name not in service_node:
|
||||
service_node[name] = {'': None}
|
||||
|
||||
service_node[name][''] = cls
|
||||
@ -196,11 +197,7 @@ class Dispatcher(View):
|
||||
This imports all packages that are descendant of this package, and, after that,
|
||||
it register all subclases of Handler. (In fact, it looks for packages inside "methods" package, child of this)
|
||||
"""
|
||||
import os.path
|
||||
import pkgutil
|
||||
import sys
|
||||
|
||||
logger.debug('Loading Handlers')
|
||||
logger.info('Initializing REST Handlers')
|
||||
|
||||
# Dinamycally import children of this package.
|
||||
package = 'methods'
|
||||
|
@ -40,6 +40,11 @@ from uds.core.auths.auth import getRootUser
|
||||
from uds.models import Authenticator
|
||||
from uds.core.managers import cryptoManager
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from django.http import HttpRequest # pylint: disable=ungrouped-imports
|
||||
from uds.models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN'
|
||||
@ -88,19 +93,26 @@ class Handler:
|
||||
raw: typing.ClassVar[bool] = False # If true, Handler will return directly an HttpResponse Object
|
||||
name: typing.ClassVar[typing.Optional[str]] = None # If name is not used, name will be the class name in lower case
|
||||
path: typing.ClassVar[typing.Optional[str]] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
|
||||
authenticated: typing.ClassVar[bool] = True # By default, all handlers needs authentication
|
||||
authenticated: bool = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff
|
||||
needs_admin: typing.ClassVar[bool] = False # By default, the methods will be accessible by anyone if nothing else indicated
|
||||
needs_staff: typing.ClassVar[bool] = False # By default, staff
|
||||
|
||||
_request: 'HttpRequest'
|
||||
_path: str
|
||||
_operation: str
|
||||
_params: typing.Any
|
||||
_args: typing.Tuple[str, ...]
|
||||
_headers: typing.Dict[str, str]
|
||||
_authToken: typing.Optional[str]
|
||||
_user: typing.Optional['User']
|
||||
|
||||
|
||||
# method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'
|
||||
def __init__(self, request, path, operation, params, *args, **kwargs):
|
||||
def __init__(self, request: 'HttpRequest', path: str, operation: str, params: typing.Any, *args, **kwargs):
|
||||
|
||||
if self.needs_admin:
|
||||
if self.needs_admin or self.needs_staff:
|
||||
self.authenticated = True # If needs_admin, must also be authenticated
|
||||
|
||||
if self.needs_staff:
|
||||
self.authenticated = True # Same for staff members
|
||||
|
||||
self._request = request
|
||||
self._path = path
|
||||
self._operation = operation
|
||||
@ -137,14 +149,14 @@ class Handler:
|
||||
"""
|
||||
return self._headers
|
||||
|
||||
def header(self, headerName):
|
||||
def header(self, headerName) -> typing.Optional[str]:
|
||||
"""
|
||||
Get's an specific header name from REST request
|
||||
:param headerName: name of header to get
|
||||
"""
|
||||
return self._headers.get(headerName)
|
||||
|
||||
def addHeader(self, header, value):
|
||||
def addHeader(self, header: str, value: str) -> None:
|
||||
"""
|
||||
Inserts a new header inside the headers list
|
||||
:param header: name of header to insert
|
||||
@ -152,7 +164,7 @@ class Handler:
|
||||
"""
|
||||
self._headers[header] = value
|
||||
|
||||
def removeHeader(self, header):
|
||||
def removeHeader(self, header: str) -> None:
|
||||
"""
|
||||
Removes an specific header from the headers list
|
||||
:param header: Name of header to remove
|
||||
@ -163,14 +175,24 @@ class Handler:
|
||||
pass # If not found, just ignore it
|
||||
|
||||
# Auth related
|
||||
def getAuthToken(self):
|
||||
def getAuthToken(self) -> typing.Optional[str]:
|
||||
"""
|
||||
Returns the authentication token for this REST request
|
||||
"""
|
||||
return self._authToken
|
||||
|
||||
@staticmethod
|
||||
def storeSessionAuthdata(session, id_auth, username, password, locale, platform, is_admin, staff_member, scrambler):
|
||||
def storeSessionAuthdata(
|
||||
session: typing.MutableMapping[str, typing.Any],
|
||||
id_auth: str,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staff_member: bool,
|
||||
scrambler: str
|
||||
):
|
||||
"""
|
||||
Stores the authentication data inside current session
|
||||
:param session: session handler (Djano user session object)
|
||||
@ -193,7 +215,17 @@ class Handler:
|
||||
'staff_member': staff_member
|
||||
}
|
||||
|
||||
def genAuthToken(self, id_auth, username, password, locale, platform, is_admin, staf_member, scrambler):
|
||||
def genAuthToken(
|
||||
self,
|
||||
id_auth: str,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staf_member: bool,
|
||||
scrambler: str
|
||||
):
|
||||
"""
|
||||
Generates the authentication token from a session, that is basically
|
||||
the session key itself
|
||||
@ -211,7 +243,7 @@ class Handler:
|
||||
self._session = session
|
||||
return self._authToken
|
||||
|
||||
def cleanAuthToken(self):
|
||||
def cleanAuthToken(self) -> None:
|
||||
"""
|
||||
Cleans up the authentication token
|
||||
"""
|
||||
@ -221,7 +253,7 @@ class Handler:
|
||||
self._session = None
|
||||
|
||||
# Session related (from auth token)
|
||||
def getValue(self, key):
|
||||
def getValue(self, key) -> typing.Optional[str]:
|
||||
"""
|
||||
Get REST session related value for a key
|
||||
"""
|
||||
@ -230,7 +262,7 @@ class Handler:
|
||||
except Exception:
|
||||
return None # _session['REST'] does not exists?
|
||||
|
||||
def setValue(self, key, value):
|
||||
def setValue(self, key: str, value: str) -> None:
|
||||
"""
|
||||
Set a session key value
|
||||
"""
|
||||
@ -241,19 +273,19 @@ class Handler:
|
||||
except Exception:
|
||||
logger.exception('Got an exception setting session value %s to %s', key, value)
|
||||
|
||||
def is_admin(self):
|
||||
def is_admin(self) -> bool:
|
||||
"""
|
||||
True if user of this REST request is administrator
|
||||
"""
|
||||
return bool(self.getValue('is_admin'))
|
||||
|
||||
def is_staff_member(self):
|
||||
def is_staff_member(self) -> bool:
|
||||
"""
|
||||
True if user of this REST request is member of staff
|
||||
"""
|
||||
return bool(self.getValue('staff_member'))
|
||||
|
||||
def getUser(self):
|
||||
def getUser(self) -> 'User':
|
||||
"""
|
||||
If user is staff member, returns his Associated user on auth
|
||||
"""
|
||||
|
@ -71,7 +71,7 @@ class Accounts(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'time_mark': item.time_mark,
|
||||
'permission': permissions.getEffectivePermission(self._user, account)
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
}
|
||||
|
||||
def getGui(self, type_):
|
||||
@ -84,4 +84,3 @@ class Accounts(ModelHandler):
|
||||
def clear(self, item):
|
||||
self.ensureAccess(item, permissions.PERMISSION_MANAGEMENT)
|
||||
return item.usages.filter(user_service=None).delete()
|
||||
|
||||
|
@ -566,8 +566,8 @@ class ModelHandler(BaseModelHandler):
|
||||
The only detail that has types within is "Service", child of "Provider"
|
||||
"""
|
||||
# Authentication related
|
||||
authenticated: typing.ClassVar[bool] = True
|
||||
needs_staff: typing.ClassVar[bool] = True
|
||||
authenticated = True
|
||||
needs_staff = True
|
||||
# Which model does this manage
|
||||
model: models.Model
|
||||
|
||||
|
@ -52,13 +52,15 @@ class ContentProcessor:
|
||||
"""
|
||||
Process contents (request/response) so Handlers can manage them
|
||||
"""
|
||||
mime_type: typing.ClassVar[typing.Optional[str]] = None
|
||||
mime_type: typing.ClassVar[str] = ''
|
||||
extensions: typing.ClassVar[typing.Iterable[str]] = []
|
||||
|
||||
def __init__(self, request):
|
||||
_request: http.HttpRequest
|
||||
|
||||
def __init__(self, request: http.HttpRequest):
|
||||
self._request = request
|
||||
|
||||
def processGetParameters(self):
|
||||
def processGetParameters(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
"""
|
||||
returns parameters based on request method
|
||||
GET parameters are understood
|
||||
@ -68,7 +70,7 @@ class ContentProcessor:
|
||||
|
||||
return self._request.GET.copy()
|
||||
|
||||
def processParameters(self):
|
||||
def processParameters(self) -> typing.Any:
|
||||
"""
|
||||
Returns the parameter from the request
|
||||
"""
|
||||
@ -81,14 +83,14 @@ class ContentProcessor:
|
||||
"""
|
||||
return http.HttpResponse(content=self.render(obj), content_type=self.mime_type + "; charset=utf-8")
|
||||
|
||||
def render(self, obj):
|
||||
def render(self, obj: typing.Any):
|
||||
"""
|
||||
Renders an obj to the spefific type
|
||||
"""
|
||||
return str(obj)
|
||||
|
||||
@staticmethod
|
||||
def procesForRender(obj):
|
||||
def procesForRender(obj: typing.Any):
|
||||
"""
|
||||
Helper for renderers. Alters some types so they can be serialized correctly (as we want them to be)
|
||||
"""
|
||||
@ -96,16 +98,10 @@ class ContentProcessor:
|
||||
return obj
|
||||
|
||||
if isinstance(obj, dict):
|
||||
res = {}
|
||||
for k, v in obj.items():
|
||||
res[k] = ContentProcessor.procesForRender(v)
|
||||
return res
|
||||
return {k:ContentProcessor.procesForRender(v) for k, v in obj.items()}
|
||||
|
||||
if isinstance(obj, (list, tuple, types.GeneratorType)):
|
||||
res = []
|
||||
for v in obj:
|
||||
res.append(ContentProcessor.procesForRender(v))
|
||||
return res
|
||||
return [ContentProcessor.procesForRender(v) for v in obj]
|
||||
|
||||
if isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return int(time.mktime(obj.timetuple()))
|
||||
@ -149,7 +145,7 @@ class JsonProcessor(MarshallerProcessor):
|
||||
"""
|
||||
mime_type = 'application/json'
|
||||
extensions = ['json']
|
||||
marshaller = json
|
||||
marshaller = json # type: ignore
|
||||
|
||||
# ---------------
|
||||
# XML Processor
|
||||
@ -166,9 +162,9 @@ class JsonProcessor(MarshallerProcessor):
|
||||
|
||||
|
||||
processors_list = (JsonProcessor,)
|
||||
default_processor = JsonProcessor
|
||||
available_processors_mime_dict = dict((cls.mime_type, cls) for cls in processors_list)
|
||||
available_processors_ext_dict = {}
|
||||
default_processor: typing.Type[ContentProcessor] = JsonProcessor
|
||||
available_processors_mime_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {cls.mime_type: cls for cls in processors_list}
|
||||
available_processors_ext_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {}
|
||||
for cls in processors_list:
|
||||
for ext in cls.extensions:
|
||||
available_processors_ext_dict[ext] = cls
|
||||
|
@ -26,7 +26,6 @@
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -34,9 +33,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RequestDebug:
|
||||
"""
|
||||
Add a X-UA-Compatible header to the response
|
||||
This header tells to Internet Explorer to render page with latest
|
||||
possible version or to use chrome frame if it is installed.
|
||||
Used for logging some request data on develeopment
|
||||
"""
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2013 Virtual Cable S.L.
|
||||
# Copyright (c) 2013-2019 Virtual Cable S.L.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
|
Loading…
Reference in New Issue
Block a user