diff --git a/server/src/uds/REST/__init__.py b/server/src/uds/REST/__init__.py index 2c6ac0f35..d2b0c1bfc 100644 --- a/server/src/uds/REST/__init__.py +++ b/server/src/uds/REST/__init__.py @@ -50,14 +50,14 @@ from .handlers import ( NotFound, RequestError, ResponseError, - NotSupportedError + NotSupportedError, ) from . import processors # Not imported at runtime, just for type checking if typing.TYPE_CHECKING: - from uds.core.util.request import ExtendedHttpRequest + from uds.core.util.request import ExtendedHttpRequestWithUser logger = logging.getLogger(__name__) @@ -70,12 +70,15 @@ class Dispatcher(View): """ This class is responsible of dispatching REST requests """ + # 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 + 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: 'ExtendedHttpRequest', *args, **kwargs): + def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args, **kwargs): """ Processes the REST request and routes it wherever it needs to be routed """ @@ -98,7 +101,9 @@ class Dispatcher(View): content_type = path[0].split('.')[1] clean_path = path[0].split('.')[0] - if not clean_path: # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients) + if ( + not clean_path + ): # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients) path = path[1:] continue @@ -115,9 +120,13 @@ class Dispatcher(View): # Here, service points to the path 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" + ) - processor = processors.available_processors_ext_dict.get(content_type, processors.default_processor)(request) + processor = processors.available_processors_ext_dict.get( + content_type, processors.default_processor + )(request) # Obtain method to be invoked http_method: str = request.method.lower() if request.method else '' @@ -128,24 +137,40 @@ class Dispatcher(View): handler = None try: - handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs) + handler = cls( + request, + full_path, + http_method, + processor.processParameters(), + *args, + **kwargs, + ) 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) - return http.HttpResponseServerError('Invalid parameters invoking {0}: {1}'.format(full_path, e), content_type="text/plain") + return http.HttpResponseServerError( + 'Invalid parameters invoking {0}: {1}'.format(full_path, e), + content_type="text/plain", + ) except AttributeError: allowedMethods = [] for n in ['get', 'post', 'put', 'delete']: if hasattr(handler, n): allowedMethods.append(n) - return http.HttpResponseNotAllowed(allowedMethods, content_type="text/plain") + return http.HttpResponseNotAllowed( + allowedMethods, content_type="text/plain" + ) except AccessDenied: - return http.HttpResponseForbidden('access denied', content_type="text/plain") + return http.HttpResponseForbidden( + 'access denied', content_type="text/plain" + ) except Exception: logger.exception('error accessing attribute') logger.debug('Getting attribute %s for %s', http_method, full_path) - return http.HttpResponseServerError('Unexcepected error', content_type="text/plain") + return http.HttpResponseServerError( + 'Unexcepected error', content_type="text/plain" + ) # Invokes the handler's operation, add headers to response and returns try: @@ -180,12 +205,16 @@ class Dispatcher(View): Try to register Handler subclasses that have not been inherited """ for cls in classes: - if not cls.__subclasses__(): # Only classes that has not been inherited will be registered as Handlers + 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) + logger.debug( + 'Adding handler %s for method %s in path %s', cls, name, cls.path + ) service_node = Dispatcher.services # Root path if cls.path: for k in cls.path.split('/'): @@ -214,7 +243,9 @@ class Dispatcher(View): pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package) for _, name, _ in pkgutil.iter_modules([pkgpath]): # __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0) - importlib.import_module( __name__ + '.' + package + '.' + name) # import module + importlib.import_module( + __name__ + '.' + package + '.' + name + ) # import module importlib.invalidate_caches() diff --git a/server/src/uds/REST/handlers.py b/server/src/uds/REST/handlers.py index be28a7e5a..643525443 100644 --- a/server/src/uds/REST/handlers.py +++ b/server/src/uds/REST/handlers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2019 Virtual Cable S.L. +# Copyright (c) 2012-2021 Virtual Cable S.L.U. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -12,7 +12,7 @@ # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -30,11 +30,9 @@ """ @author: Adolfo Gómez, dkmaster at dkmon dot com """ -import datetime import typing import logging -from django.utils import timezone from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.db import SessionStore @@ -47,7 +45,7 @@ from uds.core.managers import cryptoManager # Not imported at runtime, just for type checking if typing.TYPE_CHECKING: from uds.core.util.request import ExtendedHttpRequestWithUser - + logger = logging.getLogger(__name__) AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN' @@ -93,31 +91,59 @@ class Handler: """ REST requests handler base class """ - 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. 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 + + 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. 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: 'ExtendedHttpRequestWithUser' # It's a modified HttpRequest _path: str _operation: str _params: typing.Any # This is a deserliazied object from request. Can be anything as 'a' or {'a': 1} or .... - _args: typing.Tuple[str, ...] # This are the "path" split by /, that is, the REST invocation arguments + _args: typing.Tuple[ + str, ... + ] # This are the "path" split by /, that is, the REST invocation arguments _kwargs: typing.Dict _headers: typing.Dict[str, str] _session: typing.Optional[SessionStore] _authToken: typing.Optional[str] _user: 'User' - # method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace' - def __init__(self, request: 'ExtendedHttpRequestWithUser', path: str, operation: str, params: typing.Any, *args: str, **kwargs): + def __init__( + self, + request: 'ExtendedHttpRequestWithUser', + path: str, + operation: str, + params: typing.Any, + *args: str, + **kwargs + ): - logger.debug('Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated) - if (self.needs_admin or self.needs_staff) and not self.authenticated: # If needs_admin, must also be authenticated - raise Exception('class {} is not authenticated but has needs_admin or needs_staff set!!'.format(self.__class__)) + logger.debug( + 'Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated + ) + if ( + self.needs_admin or self.needs_staff + ) and not self.authenticated: # If needs_admin, must also be authenticated + raise Exception( + 'class {} is not authenticated but has needs_admin or needs_staff set!!'.format( + self.__class__ + ) + ) self._request = request self._path = path @@ -127,7 +153,9 @@ class Handler: self._kwargs = kwargs self._headers = {} self._authToken = None - if self.authenticated: # Only retrieve auth related data on authenticated handlers + if ( + self.authenticated + ): # Only retrieve auth related data on authenticated handlers try: self._authToken = self._request.META.get(AUTH_TOKEN_HEADER, '') self._session = SessionStore(session_key=self._authToken) @@ -150,7 +178,6 @@ class Handler: else: self._user = User() # Empty user for non authenticated handlers - def headers(self) -> typing.Dict[str, str]: """ Returns the headers of the REST request (all) @@ -191,16 +218,16 @@ class Handler: @staticmethod def storeSessionAuthdata( - session: SessionBase, - id_auth: int, - username: str, - password: str, - locale: str, - platform: str, - is_admin: bool, - staff_member: bool, - scrambler: str - ): + session: SessionBase, + id_auth: int, + 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) @@ -220,20 +247,20 @@ class Handler: 'locale': locale, 'platform': platform, 'is_admin': is_admin, - 'staff_member': staff_member + 'staff_member': staff_member, } def genAuthToken( - self, - id_auth: int, - username: str, - password: str, - locale: str, - platform: str, - is_admin: bool, - staf_member: bool, - scrambler: str - ): + self, + id_auth: int, + 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 @@ -244,11 +271,21 @@ class Handler: :param staf_member: If user is considered staff member or not """ session = SessionStore() - Handler.storeSessionAuthdata(session, id_auth, username, password, locale, platform, is_admin, staf_member, scrambler) + Handler.storeSessionAuthdata( + session, + id_auth, + username, + password, + locale, + platform, + is_admin, + staf_member, + scrambler, + ) session.save() self._authToken = session.session_key self._session = session - + return self._authToken def cleanAuthToken(self) -> None: @@ -282,13 +319,20 @@ class Handler: self._session.accessed = True self._session.save() except Exception: - logger.exception('Got an exception setting session value %s to %s', key, value) + logger.exception( + 'Got an exception setting session value %s to %s', key, value + ) def validSource(self) -> bool: try: - return net.ipInNetwork(self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True)) + return net.ipInNetwork( + self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True) + ) except Exception as e: - logger.warning('Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.', GlobalConfig.ADMIN_TRUSTED_SOURCES.get()) + logger.warning( + 'Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.', + GlobalConfig.ADMIN_TRUSTED_SOURCES.get(), + ) return True @@ -312,8 +356,10 @@ class Handler: authId = self.getValue('auth') username = self.getValue('username') # Maybe it's root user?? - if (GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True) and - username == GlobalConfig.SUPER_USER_LOGIN.get(True) and - authId == -1): + if ( + GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True) + and username == GlobalConfig.SUPER_USER_LOGIN.get(True) + and authId == -1 + ): return getRootUser() return Authenticator.objects.get(pk=authId).users.get(name=username) diff --git a/server/src/uds/REST/methods/accounts.py b/server/src/uds/REST/methods/accounts.py index 8981d03af..40cab3400 100644 --- a/server/src/uds/REST/methods/accounts.py +++ b/server/src/uds/REST/methods/accounts.py @@ -50,6 +50,7 @@ class Accounts(ModelHandler): """ Processes REST requests about accounts """ + model = Account detail = {'usage': AccountsUsage} @@ -72,7 +73,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, item) + 'permission': permissions.getEffectivePermission(self._user, item), } def getGui(self, type_: str) -> typing.List[typing.Any]: diff --git a/server/src/uds/REST/methods/accountsusage.py b/server/src/uds/REST/methods/accountsusage.py index 2592e5f43..cffcdcce8 100644 --- a/server/src/uds/REST/methods/accountsusage.py +++ b/server/src/uds/REST/methods/accountsusage.py @@ -70,7 +70,7 @@ class AccountsUsage(DetailHandler): # pylint: disable=too-many-public-methods 'running': item.user_service is not None, 'elapsed': item.elapsed, 'elapsed_timemark': item.elapsed_timemark, - 'permission': perm + 'permission': perm, } return retVal diff --git a/server/src/uds/REST/methods/actor_token.py b/server/src/uds/REST/methods/actor_token.py index 11c15b1ba..532a8ef36 100644 --- a/server/src/uds/REST/methods/actor_token.py +++ b/server/src/uds/REST/methods/actor_token.py @@ -64,7 +64,9 @@ class ActorTokens(ModelHandler): def item_as_dict(self, item: ActorToken) -> typing.Dict[str, typing.Any]: return { 'id': item.token, - 'name': _('Token isued by {} from {}').format(item.username, item.hostname or item.ip), + 'name': _('Token isued by {} from {}').format( + item.username, item.hostname or item.ip + ), 'stamp': item.stamp, 'username': item.username, 'ip': item.ip, @@ -73,7 +75,7 @@ class ActorTokens(ModelHandler): 'pre_command': item.pre_command, 'post_command': item.post_command, 'runonce_command': item.runonce_command, - 'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level%4] + 'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4], } def delete(self) -> str: @@ -83,7 +85,9 @@ class ActorTokens(ModelHandler): if len(self._args) != 1: raise RequestError('Delete need one and only one argument') - self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to delete + self.ensureAccess( + self.model(), permissions.PERMISSION_ALL, root=True + ) # Must have write permissions to delete try: self.model.objects.get(token=self._args[0]).delete() diff --git a/server/src/uds/REST/methods/calendarrules.py b/server/src/uds/REST/methods/calendarrules.py index 71630c03b..0f8278823 100644 --- a/server/src/uds/REST/methods/calendarrules.py +++ b/server/src/uds/REST/methods/calendarrules.py @@ -75,7 +75,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods 'interval': item.interval, 'duration': item.duration, 'duration_unit': item.duration_unit, - 'permission': perm + 'permission': perm, } return retVal @@ -98,7 +98,13 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods {'name': {'title': _('Rule name')}}, {'start': {'title': _('Starts'), 'type': 'datetime'}}, {'end': {'title': _('Ends'), 'type': 'date'}}, - {'frequency': {'title': _('Repeats'), 'type': 'dict', 'dict': dict((v[0], str(v[1])) for v in freqs)}}, + { + 'frequency': { + 'title': _('Repeats'), + 'type': 'dict', + 'dict': dict((v[0], str(v[1])) for v in freqs), + } + }, {'interval': {'title': _('Every'), 'type': 'callback'}}, {'duration': {'title': _('Duration'), 'type': 'callback'}}, {'comments': {'title': _('Comments')}}, @@ -108,7 +114,18 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods # Extract item db fields # We need this fields for all logger.debug('Saving rule %s / %s', parent, item) - fields = self.readFieldsFromParams(['name', 'comments', 'frequency', 'start', 'end', 'interval', 'duration', 'duration_unit']) + fields = self.readFieldsFromParams( + [ + 'name', + 'comments', + 'frequency', + 'start', + 'end', + 'interval', + 'duration', + 'duration_unit', + ] + ) if int(fields['interval']) < 1: raise self.invalidItemException('Repeat must be greater than zero') diff --git a/server/src/uds/REST/methods/calendars.py b/server/src/uds/REST/methods/calendars.py index dd82090a7..b32043d17 100644 --- a/server/src/uds/REST/methods/calendars.py +++ b/server/src/uds/REST/methods/calendars.py @@ -50,6 +50,7 @@ class Calendars(ModelHandler): """ Processes REST requests about calendars """ + model = Calendar detail = {'rules': CalendarRules} @@ -57,7 +58,14 @@ class Calendars(ModelHandler): table_title = _('Calendars') table_fields = [ - {'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-calendar text-success'}}, + { + 'name': { + 'title': _('Name'), + 'visible': True, + 'type': 'icon', + 'icon': 'fa fa-calendar text-success', + } + }, {'comments': {'title': _('Comments')}}, {'modified': {'title': _('Modified'), 'type': 'datetime'}}, {'tags': {'title': _('tags'), 'visible': False}}, @@ -70,7 +78,7 @@ class Calendars(ModelHandler): 'tags': [tag.tag for tag in item.tags.all()], 'comments': item.comments, 'modified': item.modified, - 'permission': permissions.getEffectivePermission(self._user, item) + 'permission': permissions.getEffectivePermission(self._user, item), } def getGui(self, type_: str) -> typing.List[typing.Any]: diff --git a/server/src/uds/REST/methods/client.py b/server/src/uds/REST/methods/client.py index e7cc5c041..a5a87f852 100644 --- a/server/src/uds/REST/methods/client.py +++ b/server/src/uds/REST/methods/client.py @@ -58,15 +58,16 @@ class Client(Handler): """ Processes Client requests """ + authenticated = False # Client requests are not authenticated @staticmethod def result( - result: typing.Any = None, - error: typing.Optional[typing.Union[str, int]] = None, - errorCode: int = 0, - retryable: bool = False - ) -> typing.Dict[str, typing.Any]: + result: typing.Any = None, + error: typing.Optional[typing.Union[str, int]] = None, + errorCode: int = 0, + retryable: bool = False, + ) -> typing.Dict[str, typing.Any]: """ Helper method to create a "result" set for actor response :param result: Result value to return (can be None, in which case it is converted to empty string '') @@ -84,7 +85,9 @@ class Client(Handler): if errorCode != 0: # Reformat error so it is better understood by users # error += ' (code {0:04X})'.format(errorCode) - error = _('Your service is being created. Please, wait while we complete it') + ' ({}%)'.format(int(errorCode * 25)) + error = _( + 'Your service is being created. Please, wait while we complete it' + ) + ' ({}%)'.format(int(errorCode * 25)) res['error'] = error res['retryable'] = '1' if retryable else '0' @@ -106,17 +109,26 @@ class Client(Handler): logger.debug('Client args for GET: %s', self._args) if not self._args: # Gets version - return Client.result({ - 'availableVersion': CLIENT_VERSION, - 'requiredVersion': REQUIRED_CLIENT_VERSION, - 'downloadUrl': self._request.build_absolute_uri(reverse('page.client-download')) - }) + return Client.result( + { + 'availableVersion': CLIENT_VERSION, + 'requiredVersion': REQUIRED_CLIENT_VERSION, + 'downloadUrl': self._request.build_absolute_uri( + reverse('page.client-download') + ), + } + ) if len(self._args) == 1: # Simple test return Client.result(_('Correct')) try: - ticket, scrambler = self._args # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking + ( + ticket, + scrambler, + ) = ( + self._args + ) # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking hostname = self._params['hostname'] # Or if hostname is not included... srcIp = self._request.ip @@ -127,7 +139,13 @@ class Client(Handler): except Exception: raise RequestError('Invalid request') - logger.debug('Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s', ticket, scrambler, hostname, srcIp) + logger.debug( + 'Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s', + ticket, + scrambler, + hostname, + srcIp, + ) try: data = TicketStore.get(ticket) @@ -138,10 +156,28 @@ class Client(Handler): try: logger.debug(data) - ip, userService, userServiceInstance, transport, transportInstance = userServiceManager().getService( - self._request.user, self._request.os, self._request.ip, data['service'], data['transport'], clientHostname=hostname + ( + ip, + userService, + userServiceInstance, + transport, + transportInstance, + ) = userServiceManager().getService( + self._request.user, + self._request.os, + self._request.ip, + data['service'], + data['transport'], + clientHostname=hostname, + ) + logger.debug( + 'Res: %s %s %s %s %s', + ip, + userService, + userServiceInstance, + transport, + transportInstance, ) - logger.debug('Res: %s %s %s %s %s', ip, userService, userServiceInstance, transport, transportInstance) password = cryptoManager().symDecrpyt(data['password'], scrambler) # Set "accesedByClient" @@ -155,25 +191,44 @@ class Client(Handler): if not transportInstance: raise Exception('No transport instance!!!') - transportScript, signature, params = transportInstance.getEncodedTransportScript(userService, transport, ip, self._request.os, self._request.user, password, self._request) + ( + transportScript, + signature, + params, + ) = transportInstance.getEncodedTransportScript( + userService, + transport, + ip, + self._request.os, + self._request.user, + password, + self._request, + ) logger.debug('Signature: %s', signature) logger.debug('Data:#######\n%s\n###########', params) - return Client.result(result={ - 'script': transportScript, - 'signature': signature, # It is already on base64 - 'params': codecs.encode(codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64').decode(), - }) + return Client.result( + result={ + 'script': transportScript, + 'signature': signature, # It is already on base64 + 'params': codecs.encode( + codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64' + ).decode(), + } + ) except ServiceNotReadyError as e: # Set that client has accesed userService if e.userService: e.userService.setProperty('accessedByClient', '1') # Refresh ticket and make this retrayable - TicketStore.revalidate(ticket, 20) # Retry will be in at most 5 seconds, so 20 is fine :) - return Client.result(error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True) + TicketStore.revalidate( + ticket, 20 + ) # Retry will be in at most 5 seconds, so 20 is fine :) + return Client.result( + error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True + ) except Exception as e: logger.exception("Exception") return Client.result(error=str(e)) - diff --git a/server/src/uds/REST/methods/config.py b/server/src/uds/REST/methods/config.py index 1ec0d9f0f..837ca912a 100644 --- a/server/src/uds/REST/methods/config.py +++ b/server/src/uds/REST/methods/config.py @@ -42,9 +42,15 @@ logger = logging.getLogger(__name__) # Pair of section/value removed from current UDS version REMOVED = { 'UDS': ( - 'allowPreferencesAccess', 'customHtmlLogin', 'UDS Theme', - 'UDS Theme Enhaced', 'css', 'allowPreferencesAccess', - 'loginUrl', 'maxLoginTries', 'loginBlockTime' + 'allowPreferencesAccess', + 'customHtmlLogin', + 'UDS Theme', + 'UDS Theme Enhaced', + 'css', + 'allowPreferencesAccess', + 'loginUrl', + 'maxLoginTries', + 'loginBlockTime', ), 'Cluster': ('Destination CPU Load', 'Migration CPU Load', 'Migration Free Memory'), 'IPAUTH': ('autoLogin',), @@ -81,7 +87,7 @@ class Config(Handler): 'crypt': cfg.isCrypted(), 'longText': cfg.isLongText(), 'type': cfg.getType(), - 'params': cfg.getParams() + 'params': cfg.getParams(), } logger.debug('Configuration: %s', res) return res diff --git a/server/src/uds/REST/methods/connection.py b/server/src/uds/REST/methods/connection.py index d1bf9c3f2..1cbaa3a79 100644 --- a/server/src/uds/REST/methods/connection.py +++ b/server/src/uds/REST/methods/connection.py @@ -88,7 +88,11 @@ class Connection(Handler): # Ensure user is present on request, used by web views methods self._request.user = self._user - return Connection.result(result=services.getServicesData(typing.cast(ExtendedHttpRequestWithUser, self._request))) + return Connection.result( + result=services.getServicesData( + typing.cast(ExtendedHttpRequestWithUser, self._request) + ) + ) def connection(self, doNotCheck: bool = False): idService = self._args[0] @@ -183,7 +187,9 @@ class Connection(Handler): self._request.user = self._user # type: ignore self._request._cryptedpass = self._session['REST']['password'] # type: ignore self._request._scrambler = self._request.META['HTTP_SCRAMBLER'] # type: ignore - linkInfo = services.enableService(self._request, idService=self._args[0], idTransport=self._args[1]) + linkInfo = services.enableService( + self._request, idService=self._args[0], idTransport=self._args[1] + ) if linkInfo['error']: return Connection.result(error=linkInfo['error']) return Connection.result(result=linkInfo['url']) diff --git a/server/src/uds/REST/methods/images.py b/server/src/uds/REST/methods/images.py index 9ada47739..9ae4bcdbf 100644 --- a/server/src/uds/REST/methods/images.py +++ b/server/src/uds/REST/methods/images.py @@ -49,13 +49,21 @@ class Images(ModelHandler): """ Handles the gallery REST interface """ + path = 'gallery' model = Image save_fields = ['name', 'data'] table_title = _('Image Gallery') table_fields = [ - {'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}}, + { + 'thumb': { + 'title': _('Image'), + 'visible': True, + 'type': 'image', + 'width': '96px', + } + }, {'name': {'title': _('Name')}}, {'size': {'title': _('Size')}}, ] @@ -69,17 +77,17 @@ class Images(ModelHandler): item.updateThumbnail() item.save() - def getGui(self, type_: str) -> typing.List[typing.Any]: return self.addField( - self.addDefaultFields([], ['name']), { + self.addDefaultFields([], ['name']), + { 'name': 'data', 'value': '', 'label': ugettext('Image'), 'tooltip': ugettext('Image object'), 'type': gui.InputField.IMAGECHOICE_TYPE, 'order': 100, # At end - } + }, ) def item_as_dict(self, item: Image) -> typing.Dict[str, typing.Any]: @@ -92,7 +100,9 @@ class Images(ModelHandler): def item_as_dict_overview(self, item: Image) -> typing.Dict[str, typing.Any]: return { 'id': item.uuid, - 'size': '{}x{}, {} bytes (thumb {} bytes)'.format(item.width, item.height, len(item.data), len(item.thumb)), + 'size': '{}x{}, {} bytes (thumb {} bytes)'.format( + item.width, item.height, len(item.data), len(item.thumb) + ), 'name': item.name, 'thumb': item.thumb64, } diff --git a/server/src/uds/REST/methods/login_logout.py b/server/src/uds/REST/methods/login_logout.py index 1dce72796..2c4d3d33a 100644 --- a/server/src/uds/REST/methods/login_logout.py +++ b/server/src/uds/REST/methods/login_logout.py @@ -53,15 +53,22 @@ logger = logging.getLogger(__name__) # Enclosed methods under /auth path + class Login(Handler): """ Responsible of user authentication """ + path = 'auth' authenticated = False # Public method @staticmethod - def result(result: str = 'error', token: str = None, scrambler: str = None, error: str = None) -> typing.MutableMapping[str, typing.Any]: + def result( + result: str = 'error', + token: str = None, + scrambler: str = None, + error: str = None, + ) -> typing.MutableMapping[str, typing.Any]: res = { 'result': result, 'token': token, @@ -109,15 +116,31 @@ class Login(Handler): cache = Cache('RESTapi') fails = cache.get(self._request.ip) or 0 if fails > ALLOWED_FAILS: - logger.info('Access to REST API %s is blocked for %s seconds since last fail', self._request.ip, GlobalConfig.LOGIN_BLOCK.getInt()) - + logger.info( + 'Access to REST API %s is blocked for %s seconds since last fail', + self._request.ip, + GlobalConfig.LOGIN_BLOCK.getInt(), + ) + try: - if 'auth_id' not in self._params and 'authId' not in self._params and 'authSmallName' not in self._params and 'auth' not in self._params: + if ( + 'auth_id' not in self._params + and 'authId' not in self._params + and 'authSmallName' not in self._params + and 'auth' not in self._params + ): raise RequestError('Invalid parameters (no auth)') - scrambler: str = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(32)) # @UndefinedVariable - authId: typing.Optional[str] = self._params.get('authId', self._params.get('auth_id', None)) - authSmallName: typing.Optional[str] = self._params.get('authSmallName', None) + scrambler: str = ''.join( + random.SystemRandom().choice(string.ascii_letters + string.digits) + for _ in range(32) + ) # @UndefinedVariable + authId: typing.Optional[str] = self._params.get( + 'authId', self._params.get('auth_id', None) + ) + authSmallName: typing.Optional[str] = self._params.get( + 'authSmallName', None + ) authName: typing.Optional[str] = self._params.get('auth', None) platform: str = self._params.get('platform', self._request.os) @@ -126,9 +149,18 @@ class Login(Handler): username, password = self._params['username'], self._params['password'] locale: str = self._params.get('locale', 'en') - if authName == 'admin' or authSmallName == 'admin' or authId == '00000000-0000-0000-0000-000000000000': - if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and GlobalConfig.SUPER_USER_PASS.get(True) == password: - self.genAuthToken(-1, username, password, locale, platform, True, True, scrambler) + if ( + authName == 'admin' + or authSmallName == 'admin' + or authId == '00000000-0000-0000-0000-000000000000' + ): + if ( + GlobalConfig.SUPER_USER_LOGIN.get(True) == username + and GlobalConfig.SUPER_USER_PASS.get(True) == password + ): + self.genAuthToken( + -1, username, password, locale, platform, True, True, scrambler + ) return Login.result(result='ok', token=self.getAuthToken()) return Login.result(error='Invalid credentials') @@ -149,13 +181,24 @@ class Login(Handler): # Sleep a while here to "prottect" time.sleep(3) # Wait 3 seconds if credentials fails for "protection" # And store in cache for blocking for a while if fails - cache.put(self._request.ip, fails+1, GlobalConfig.LOGIN_BLOCK.getInt()) - + cache.put( + self._request.ip, fails + 1, GlobalConfig.LOGIN_BLOCK.getInt() + ) + return Login.result(error='Invalid credentials') return Login.result( result='ok', - token=self.genAuthToken(auth.id, user.name, password, locale, platform, user.is_admin, user.staff_member, scrambler), - scrambler=scrambler + token=self.genAuthToken( + auth.id, + user.name, + password, + locale, + platform, + user.is_admin, + user.staff_member, + scrambler, + ), + scrambler=scrambler, ) except Exception: @@ -169,6 +212,7 @@ class Logout(Handler): """ Responsible of user de-authentication """ + path = 'auth' authenticated = True # By default, all handlers needs authentication @@ -190,14 +234,16 @@ class Auths(Handler): auth: Authenticator for auth in Authenticator.objects.all(): theType = auth.getType() - if paramAll or (theType.isCustom() is False and theType.typeType not in ('IP',)): + if paramAll or ( + theType.isCustom() is False and theType.typeType not in ('IP',) + ): yield { 'authId': auth.uuid, 'authSmallName': str(auth.small_name), 'auth': auth.name, 'type': theType.typeType, 'priority': auth.priority, - 'isCustom': theType.isCustom() + 'isCustom': theType.isCustom(), } def get(self): diff --git a/server/src/uds/REST/methods/meta_pools.py b/server/src/uds/REST/methods/meta_pools.py index 3166b8382..4442ebf32 100644 --- a/server/src/uds/REST/methods/meta_pools.py +++ b/server/src/uds/REST/methods/meta_pools.py @@ -54,6 +54,7 @@ class MetaPools(ModelHandler): """ Handles Services Pools REST requests """ + model = MetaPool detail = { 'pools': MetaServicesPool, @@ -62,8 +63,18 @@ class MetaPools(ModelHandler): 'access': AccessCalendars, } - save_fields = ['name', 'short_name', 'comments', 'tags', - 'image_id', 'servicesPoolGroup_id', 'visible', 'policy', 'calendar_message', 'transport_grouping'] + save_fields = [ + 'name', + 'short_name', + 'comments', + 'tags', + 'image_id', + 'servicesPoolGroup_id', + 'visible', + 'policy', + 'calendar_message', + 'transport_grouping', + ] table_title = _('Meta Pools') table_fields = [ @@ -93,8 +104,16 @@ class MetaPools(ModelHandler): poolGroupThumb = item.servicesPoolGroup.image.thumb64 allPools = item.members.all() - userServicesCount = sum((i.pool.userServices.exclude(state__in=State.INFO_STATES).count() for i in allPools)) - userServicesInPreparation = sum((i.pool.userServices.filter(state=State.PREPARING).count()) for i in allPools) + userServicesCount = sum( + ( + i.pool.userServices.exclude(state__in=State.INFO_STATES).count() + for i in allPools + ) + ) + userServicesInPreparation = sum( + (i.pool.userServices.filter(state=State.PREPARING).count()) + for i in allPools + ) val = { 'id': item.uuid, @@ -102,7 +121,9 @@ class MetaPools(ModelHandler): 'short_name': item.short_name, 'tags': [tag.tag for tag in item.tags.all()], 'comments': item.comments, - 'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64, + 'thumb': item.image.thumb64 + if item.image is not None + else DEFAULT_THUMB_BASE64, 'image_id': item.image.uuid if item.image is not None else None, 'servicesPoolGroup_id': poolGroupId, 'pool_group_name': poolGroupName, @@ -114,7 +135,7 @@ class MetaPools(ModelHandler): 'fallbackAccess': item.fallbackAccess, 'permission': permissions.getEffectivePermission(self._user, item), 'calendar_message': item.calendar_message, - 'transport_grouping': item.transport_grouping + 'transport_grouping': item.transport_grouping, } return val @@ -123,30 +144,50 @@ class MetaPools(ModelHandler): def getGui(self, type_: str) -> typing.List[typing.Any]: localGUI = self.addDefaultFields([], ['name', 'short_name', 'comments', 'tags']) - for field in [{ + for field in [ + { 'name': 'policy', - 'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()], + 'values': [ + gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items() + ], 'label': ugettext('Policy'), 'tooltip': ugettext('Service pool policy'), 'type': gui.InputField.CHOICE_TYPE, 'order': 100, - }, { + }, + { 'name': 'image_id', - 'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]), + 'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + + gui.sortedChoices( + [ + gui.choiceImage(v.uuid, v.name, v.thumb64) + for v in Image.objects.all() + ] + ), 'label': ugettext('Associated Image'), 'tooltip': ugettext('Image assocciated with this service'), 'type': gui.InputField.IMAGECHOICE_TYPE, 'order': 120, 'tab': gui.DISPLAY_TAB, - }, { + }, + { 'name': 'servicesPoolGroup_id', - 'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in ServicePoolGroup.objects.all()]), + 'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)] + + gui.sortedChoices( + [ + gui.choiceImage(v.uuid, v.name, v.thumb64) + for v in ServicePoolGroup.objects.all() + ] + ), 'label': ugettext('Pool group'), - 'tooltip': ugettext('Pool group for this pool (for pool classify on display)'), + 'tooltip': ugettext( + 'Pool group for this pool (for pool classify on display)' + ), 'type': gui.InputField.IMAGECHOICE_TYPE, 'order': 121, 'tab': gui.DISPLAY_TAB, - }, { + }, + { 'name': 'visible', 'value': True, 'label': ugettext('Visible'), @@ -154,23 +195,31 @@ class MetaPools(ModelHandler): 'type': gui.InputField.CHECKBOX_TYPE, 'order': 123, 'tab': gui.DISPLAY_TAB, - }, { + }, + { 'name': 'calendar_message', 'value': '', 'label': ugettext('Calendar access denied text'), - 'tooltip': ugettext('Custom message to be shown to users if access is limited by calendar rules.'), + 'tooltip': ugettext( + 'Custom message to be shown to users if access is limited by calendar rules.' + ), 'type': gui.InputField.TEXT_TYPE, 'order': 124, 'tab': gui.DISPLAY_TAB, - }, { + }, + { 'name': 'transport_grouping', - 'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TRANSPORT_SELECT.items()], + 'values': [ + gui.choiceItem(k, str(v)) + for k, v in MetaPool.TRANSPORT_SELECT.items() + ], 'label': ugettext('Transport Selection'), 'tooltip': ugettext('Transport selection policy'), 'type': gui.InputField.CHOICE_TYPE, 'order': 125, - 'tab': gui.DISPLAY_TAB - }]: + 'tab': gui.DISPLAY_TAB, + }, + ]: self.addField(localGUI, field) return localGUI diff --git a/server/src/uds/REST/methods/networks.py b/server/src/uds/REST/methods/networks.py index 064beb2ca..01391b39e 100644 --- a/server/src/uds/REST/methods/networks.py +++ b/server/src/uds/REST/methods/networks.py @@ -52,12 +52,20 @@ class Networks(ModelHandler): Processes REST requests about networks Implements specific handling for network related requests using GUI """ + model = Network save_fields = ['name', 'net_string', 'tags'] table_title = _('Networks') table_fields = [ - {'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-globe text-success'}}, + { + 'name': { + 'title': _('Name'), + 'visible': True, + 'type': 'icon', + 'icon': 'fa fa-globe text-success', + } + }, {'net_string': {'title': _('Range')}}, {'networks_count': {'title': _('Used by'), 'type': 'numeric', 'width': '8em'}}, {'tags': {'title': _('tags'), 'visible': False}}, @@ -75,14 +83,17 @@ class Networks(ModelHandler): def getGui(self, type_: str) -> typing.List[typing.Any]: return self.addField( - self.addDefaultFields([], ['name', 'tags']), { + self.addDefaultFields([], ['name', 'tags']), + { 'name': 'net_string', 'value': '', 'label': ugettext('Network range'), - 'tooltip': ugettext('Network range. Accepts most network definitions formats (range, subnet, host, etc...'), + 'tooltip': ugettext( + 'Network range. Accepts most network definitions formats (range, subnet, host, etc...' + ), 'type': gui.InputField.TEXT_TYPE, 'order': 100, # At end - } + }, ) def item_as_dict(self, item: Network) -> typing.Dict[str, typing.Any]: @@ -92,5 +103,5 @@ class Networks(ModelHandler): 'tags': [tag.tag for tag in item.tags.all()], 'net_string': item.net_string, 'networks_count': item.transports.count(), - 'permission': permissions.getEffectivePermission(self._user, item) + 'permission': permissions.getEffectivePermission(self._user, item), } diff --git a/server/src/uds/REST/methods/op_calendars.py b/server/src/uds/REST/methods/op_calendars.py index 86796ba9d..d7a54362e 100644 --- a/server/src/uds/REST/methods/op_calendars.py +++ b/server/src/uds/REST/methods/op_calendars.py @@ -52,6 +52,7 @@ logger = logging.getLogger(__name__) ALLOW = 'ALLOW' DENY = 'DENY' + class AccessCalendars(DetailHandler): @staticmethod def as_dict(item: 'CalendarAccess'): @@ -67,7 +68,9 @@ class AccessCalendars(DetailHandler): try: if not item: return [AccessCalendars.as_dict(i) for i in parent.calendarAccess.all()] - return AccessCalendars.as_dict(parent.calendarAccess.get(uuid=processUuid(item))) + return AccessCalendars.as_dict( + parent.calendarAccess.get(uuid=processUuid(item)) + ) except Exception: logger.exception('err: %s', item) raise self.invalidItemException() @@ -87,7 +90,9 @@ class AccessCalendars(DetailHandler): uuid = processUuid(item) if item is not None else None try: - calendar: Calendar = Calendar.objects.get(uuid=processUuid(self._params['calendarId'])) + calendar: Calendar = Calendar.objects.get( + uuid=processUuid(self._params['calendarId']) + ) access: str = self._params['access'].upper() if access not in (ALLOW, DENY): raise Exception() @@ -103,13 +108,24 @@ class AccessCalendars(DetailHandler): calAccess.priority = priority calAccess.save() else: - parent.calendarAccess.create(calendar=calendar, access=access, priority=priority) + parent.calendarAccess.create( + calendar=calendar, access=access, priority=priority + ) - log.doLog(parent, log.INFO, "Added access calendar {}/{} by {}".format(calendar.name, access, self._user.pretty_name), log.ADMIN) + log.doLog( + parent, + log.INFO, + "Added access calendar {}/{} by {}".format( + calendar.name, access, self._user.pretty_name + ), + log.ADMIN, + ) def deleteItem(self, parent: 'ServicePool', item: str) -> None: calendarAccess = parent.calendarAccess.get(uuid=processUuid(self._args[0])) - logStr = "Removed access calendar {} by {}".format(calendarAccess.calendar.name, self._user.pretty_name) + logStr = "Removed access calendar {} by {}".format( + calendarAccess.calendar.name, self._user.pretty_name + ) calendarAccess.delete() @@ -120,7 +136,10 @@ class ActionsCalendars(DetailHandler): """ Processes the transports detail requests of a Service Pool """ - custom_methods = ['execute',] + + custom_methods = [ + 'execute', + ] @staticmethod def as_dict(item: 'CalendarAction') -> typing.Dict[str, typing.Any]: @@ -131,19 +150,21 @@ class ActionsCalendars(DetailHandler): 'calendarId': item.calendar.uuid, 'calendar': item.calendar.name, 'action': item.action, - 'actionDescription': action.get('description'), + 'actionDescription': action.get('description'), 'atStart': item.at_start, 'eventsOffset': item.events_offset, 'params': params, 'pretty_params': item.prettyParams, 'nextExecution': item.next_execution, - 'lastExecution': item.last_execution + 'lastExecution': item.last_execution, } def getItems(self, parent: 'ServicePool', item: typing.Optional[str]): try: if item is None: - return [ActionsCalendars.as_dict(i) for i in parent.calendaraction_set.all()] + return [ + ActionsCalendars.as_dict(i) for i in parent.calendaraction_set.all() + ] i = parent.calendaraction_set.get(uuid=processUuid(item)) return ActionsCalendars.as_dict(i) except Exception: @@ -177,8 +198,12 @@ class ActionsCalendars(DetailHandler): # logger.debug('Got parameters: {} {} {} {} ----> {}'.format(calendar, action, eventsOffset, atStart, params)) logStr = "Added scheduled action \"{},{},{},{},{}\" by {}".format( - calendar.name, action, eventsOffset, - atStart and 'Start' or 'End', params, self._user.pretty_name + calendar.name, + action, + eventsOffset, + atStart and 'Start' or 'End', + params, + self._user.pretty_name, ) if uuid is not None: @@ -191,16 +216,26 @@ class ActionsCalendars(DetailHandler): calAction.params = params calAction.save() else: - CalendarAction.objects.create(calendar=calendar, service_pool=parent, action=action, at_start=atStart, events_offset=eventsOffset, params=params) + CalendarAction.objects.create( + calendar=calendar, + service_pool=parent, + action=action, + at_start=atStart, + events_offset=eventsOffset, + params=params, + ) log.doLog(parent, log.INFO, logStr, log.ADMIN) def deleteItem(self, parent: 'ServicePool', item: str) -> None: calendarAction = CalendarAction.objects.get(uuid=processUuid(self._args[0])) logStr = "Removed scheduled action \"{},{},{},{},{}\" by {}".format( - calendarAction.calendar.name, calendarAction.action, - calendarAction.events_offset, calendarAction.at_start and 'Start' or 'End', calendarAction.params, - self._user.pretty_name + calendarAction.calendar.name, + calendarAction.action, + calendarAction.events_offset, + calendarAction.at_start and 'Start' or 'End', + calendarAction.params, + self._user.pretty_name, ) calendarAction.delete() @@ -213,11 +248,14 @@ class ActionsCalendars(DetailHandler): calendarAction: CalendarAction = CalendarAction.objects.get(uuid=uuid) self.ensureAccess(calendarAction, permissions.PERMISSION_MANAGEMENT) logStr = "Launched scheduled action \"{},{},{},{},{}\" by {}".format( - calendarAction.calendar.name, calendarAction.action, - calendarAction.events_offset, calendarAction.at_start and 'Start' or 'End', calendarAction.params, - self._user.pretty_name + calendarAction.calendar.name, + calendarAction.action, + calendarAction.events_offset, + calendarAction.at_start and 'Start' or 'End', + calendarAction.params, + self._user.pretty_name, ) - + calendarAction.execute() log.doLog(parent, log.INFO, logStr, log.ADMIN) diff --git a/server/src/uds/REST/methods/osmanagers.py b/server/src/uds/REST/methods/osmanagers.py index 2681567f9..7969ce80e 100644 --- a/server/src/uds/REST/methods/osmanagers.py +++ b/server/src/uds/REST/methods/osmanagers.py @@ -70,7 +70,7 @@ class OsManagers(ModelHandler): 'type_name': type_.name(), 'servicesTypes': type_.servicesType, 'comments': osm.comments, - 'permission': permissions.getEffectivePermission(self._user, osm) + 'permission': permissions.getEffectivePermission(self._user, osm), } def item_as_dict(self, item: OSManager) -> typing.Dict[str, typing.Any]: @@ -79,7 +79,9 @@ class OsManagers(ModelHandler): def checkDelete(self, item: OSManager) -> None: # Only can delete if no ServicePools attached if item.deployedServices.count() > 0: - raise RequestError(ugettext('Can\'t delete an OS Manager with services pools associated')) + raise RequestError( + ugettext('Can\'t delete an OS Manager with services pools associated') + ) # Types related def enum_types(self) -> typing.Iterable[typing.Type[osmanagers.OSManager]]: @@ -88,6 +90,9 @@ class OsManagers(ModelHandler): # Gui related def getGui(self, type_: str) -> typing.List[typing.Any]: try: - return self.addDefaultFields(osmanagers.factory().lookup(type_).guiDescription(), ['name', 'comments', 'tags']) + return self.addDefaultFields( + osmanagers.factory().lookup(type_).guiDescription(), # type: ignore # may raise an exception if lookup fails + ['name', 'comments', 'tags'], + ) except: raise NotFound('type not found') diff --git a/server/src/uds/REST/methods/proxies.py b/server/src/uds/REST/methods/proxies.py index 88512d8b2..1046f8f0e 100644 --- a/server/src/uds/REST/methods/proxies.py +++ b/server/src/uds/REST/methods/proxies.py @@ -50,6 +50,7 @@ class Proxies(ModelHandler): """ Processes REST requests about proxys """ + model = Proxy save_fields = ['name', 'host', 'port', 'ssl', 'check_cert', 'comments', 'tags'] @@ -74,42 +75,49 @@ class Proxies(ModelHandler): 'port': item.port, 'ssl': item.ssl, 'check_cert': item.check_cert, - 'permission': permissions.getEffectivePermission(self._user, item) + 'permission': permissions.getEffectivePermission(self._user, item), } def getGui(self, type_: str) -> typing.List[typing.Any]: g = self.addDefaultFields([], ['name', 'comments', 'tags']) for f in [ - { - 'name': 'host', - 'value': '', - 'label': ugettext('Host'), - 'tooltip': ugettext('Server (IP or FQDN) that will serve as proxy.'), - 'type': gui.InputField.TEXT_TYPE, - 'order': 110, - }, { - 'name': 'port', - 'value': '9090', - 'minValue': '0', - 'label': ugettext('Port'), - 'tooltip': ugettext('Port of proxy server'), - 'type': gui.InputField.NUMERIC_TYPE, - 'order': 111, - }, { - 'name': 'ssl', - 'value': True, - 'label': ugettext('Use SSL'), - 'tooltip': ugettext('If active, the proxied connections will be done using HTTPS'), - 'type': gui.InputField.CHECKBOX_TYPE, - }, { - 'name': 'check_cert', - 'value': True, - 'label': ugettext('Check Certificate'), - 'tooltip': ugettext('If active, any SSL certificate will be checked (will not allow self signed certificates on proxy)'), - 'type': gui.InputField.CHECKBOX_TYPE, - }, - ]: + { + 'name': 'host', + 'value': '', + 'label': ugettext('Host'), + 'tooltip': ugettext('Server (IP or FQDN) that will serve as proxy.'), + 'type': gui.InputField.TEXT_TYPE, + 'order': 110, + }, + { + 'name': 'port', + 'value': '9090', + 'minValue': '0', + 'label': ugettext('Port'), + 'tooltip': ugettext('Port of proxy server'), + 'type': gui.InputField.NUMERIC_TYPE, + 'order': 111, + }, + { + 'name': 'ssl', + 'value': True, + 'label': ugettext('Use SSL'), + 'tooltip': ugettext( + 'If active, the proxied connections will be done using HTTPS' + ), + 'type': gui.InputField.CHECKBOX_TYPE, + }, + { + 'name': 'check_cert', + 'value': True, + 'label': ugettext('Check Certificate'), + 'tooltip': ugettext( + 'If active, any SSL certificate will be checked (will not allow self signed certificates on proxy)' + ), + 'type': gui.InputField.CHECKBOX_TYPE, + }, + ]: self.addField(g, f) return g diff --git a/server/src/uds/REST/methods/reports.py b/server/src/uds/REST/methods/reports.py index f9a06516e..d35195b59 100644 --- a/server/src/uds/REST/methods/reports.py +++ b/server/src/uds/REST/methods/reports.py @@ -41,7 +41,17 @@ from uds import reports logger = logging.getLogger(__name__) -VALID_PARAMS = ('authId', 'authSmallName', 'auth', 'username', 'realname', 'password', 'groups', 'servicePool', 'transport') +VALID_PARAMS = ( + 'authId', + 'authSmallName', + 'auth', + 'username', + 'realname', + 'password', + 'groups', + 'servicePool', + 'transport', +) # Enclosed methods under /actor path @@ -49,14 +59,21 @@ class Reports(model.BaseModelHandler): """ Processes actor requests """ + needs_admin = True # By default, staff is lower level needed table_title = _('Available reports') table_fields = [ {'group': {'title': _('Group')}}, - {'name': {'title': _('Name')}}, # Will process this field on client in fact, not sent by server - {'description': {'title': _('Description')}}, # Will process this field on client in fact, not sent by server - {'mime_type': {'title': _('Generates')}}, # Will process this field on client in fact, not sent by server + { + 'name': {'title': _('Name')} + }, # Will process this field on client in fact, not sent by server + { + 'description': {'title': _('Description')} + }, # Will process this field on client in fact, not sent by server + { + 'mime_type': {'title': _('Generates')} + }, # Will process this field on client in fact, not sent by server ] # Field from where to get "class" and prefix for that class, so this will generate "row-state-A, row-state-X, .... table_row_style = {'field': 'state', 'prefix': 'row-state-'} @@ -85,7 +102,9 @@ class Reports(model.BaseModelHandler): if self._args[0] == model.OVERVIEW: return list(self.getItems()) elif self._args[0] == model.TABLEINFO: - return self.processTableFields(self.table_title, self.table_fields, self.table_row_style) + return self.processTableFields( + self.table_title, self.table_fields, self.table_row_style + ) if nArgs == 2: if self._args[0] == model.GUI: @@ -97,7 +116,12 @@ class Reports(model.BaseModelHandler): """ Processes a PUT request """ - logger.debug('method PUT for %s, %s, %s', self.__class__.__name__, self._args, self._params) + logger.debug( + 'method PUT for %s, %s, %s', + self.__class__.__name__, + self._args, + self._params, + ) if len(self._args) != 1: raise self.invalidRequestException() @@ -112,7 +136,7 @@ class Reports(model.BaseModelHandler): 'mime_type': report.mime_type, 'encoded': report.encoded, 'filename': report.filename, - 'data': result + 'data': result, } return data @@ -126,7 +150,9 @@ class Reports(model.BaseModelHandler): return sorted(report.guiDescription(report), key=lambda f: f['gui']['order']) # Returns the list of - def getItems(self, *args, **kwargs) -> typing.Generator[typing.Dict[str, typing.Any], None, None]: + def getItems( + self, *args, **kwargs + ) -> typing.Generator[typing.Dict[str, typing.Any], None, None]: for i in reports.availableReports: yield { 'id': i.getUuid(), @@ -134,5 +160,5 @@ class Reports(model.BaseModelHandler): 'encoded': i.encoded, 'group': i.translated_group(), 'name': i.translated_name(), - 'description': i.translated_description() + 'description': i.translated_description(), } diff --git a/server/src/uds/REST/methods/services_pool_groups.py b/server/src/uds/REST/methods/services_pool_groups.py index 244d6705c..110b83feb 100644 --- a/server/src/uds/REST/methods/services_pool_groups.py +++ b/server/src/uds/REST/methods/services_pool_groups.py @@ -50,6 +50,7 @@ class ServicesPoolGroups(ModelHandler): """ Handles the gallery REST interface """ + # needs_admin = True path = 'gallery' @@ -59,7 +60,14 @@ class ServicesPoolGroups(ModelHandler): table_title = _('Services Pool Groups') table_fields = [ {'priority': {'title': _('Priority'), 'type': 'numeric', 'width': '6em'}}, - {'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}}, + { + 'thumb': { + 'title': _('Image'), + 'visible': True, + 'type': 'image', + 'width': '96px', + } + }, {'name': {'title': _('Name')}}, {'comments': {'title': _('Comments')}}, ] @@ -79,14 +87,22 @@ class ServicesPoolGroups(ModelHandler): def getGui(self, type_: str) -> typing.List[typing.Any]: localGui = self.addDefaultFields([], ['name', 'comments', 'priority']) - for field in [{ + for field in [ + { 'name': 'image_id', - 'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]), + 'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + + gui.sortedChoices( + [ + gui.choiceImage(v.uuid, v.name, v.thumb64) + for v in Image.objects.all() + ] + ), 'label': ugettext('Associated Image'), 'tooltip': ugettext('Image assocciated with this service'), 'type': gui.InputField.IMAGECHOICE_TYPE, 'order': 102, - }]: + } + ]: self.addField(localGui, field) return localGui @@ -100,7 +116,9 @@ class ServicesPoolGroups(ModelHandler): 'image_id': item.image.uuid if item.image else None, } - def item_as_dict_overview(self, item: ServicePoolGroup) -> typing.Dict[str, typing.Any]: + def item_as_dict_overview( + self, item: ServicePoolGroup + ) -> typing.Dict[str, typing.Any]: return { 'id': item.uuid, 'priority': item.priority, diff --git a/server/src/uds/REST/methods/services_usage.py b/server/src/uds/REST/methods/services_usage.py index 8b8fb541f..092d49f59 100644 --- a/server/src/uds/REST/methods/services_usage.py +++ b/server/src/uds/REST/methods/services_usage.py @@ -64,16 +64,10 @@ class ServicesUsage(DetailHandler): if item.user is None: owner = '' - owner_info = { - 'auth_id': '', - 'user_id': '' - } + owner_info = {'auth_id': '', 'user_id': ''} else: owner = item.user.pretty_name - owner_info = { - 'auth_id': item.user.manager.uuid, - 'user_id': item.user.uuid - } + owner_info = {'auth_id': item.user.manager.uuid, 'user_id': item.user.uuid} return { 'id': item.uuid, @@ -90,19 +84,30 @@ class ServicesUsage(DetailHandler): 'ip': props.get('ip', _('unknown')), 'source_host': item.src_hostname, 'source_ip': item.src_ip, - 'in_use': item.in_use + 'in_use': item.in_use, } def getItems(self, parent: 'Provider', item: typing.Optional[str]): try: if item is None: - userServicesQuery = UserService.objects.filter(deployed_service__service__provider=parent) + userServicesQuery = UserService.objects.filter( + deployed_service__service__provider=parent + ) else: - userServicesQuery = UserService.objects.filter(deployed_service__service_uuid=processUuid(item)) + userServicesQuery = UserService.objects.filter( + deployed_service__service_uuid=processUuid(item) + ) - return [ServicesUsage.itemToDict(k) for k in userServicesQuery.filter(state=State.USABLE).order_by('creation_date'). - prefetch_related('deployed_service').prefetch_related('deployed_service__service').prefetch_related('properties'). - prefetch_related('user').prefetch_related('user__manager')] + return [ + ServicesUsage.itemToDict(k) + for k in userServicesQuery.filter(state=State.USABLE) + .order_by('creation_date') + .prefetch_related('deployed_service') + .prefetch_related('deployed_service__service') + .prefetch_related('properties') + .prefetch_related('user') + .prefetch_related('user__manager') + ] except Exception: logger.exception('getItems') @@ -131,7 +136,9 @@ class ServicesUsage(DetailHandler): def deleteItem(self, parent: 'Provider', item: str) -> None: userService: UserService try: - userService = UserService.objects.get(uuid=processUuid(item), deployed_service__service__provider=parent) + userService = UserService.objects.get( + uuid=processUuid(item), deployed_service__service__provider=parent + ) except Exception: raise self.invalidItemException() diff --git a/server/src/uds/REST/methods/system.py b/server/src/uds/REST/methods/system.py index bc159b914..3c376ed54 100644 --- a/server/src/uds/REST/methods/system.py +++ b/server/src/uds/REST/methods/system.py @@ -59,7 +59,9 @@ USE_MAX = True def getServicesPoolsCounters( - servicePool: typing.Optional[models.ServicePool], counter_type: int, since_days: int = SINCE + servicePool: typing.Optional[models.ServicePool], + counter_type: int, + since_days: int = SINCE, ) -> typing.List[typing.Mapping[str, typing.Any]]: try: cacheKey = ( @@ -142,14 +144,18 @@ class System(Handler): pool: typing.Optional[models.ServicePool] = None if len(self._args) == 3: try: - pool = models.ServicePool.objects.get(uuid=processUuid(self._args[2])) + pool = models.ServicePool.objects.get( + uuid=processUuid(self._args[2]) + ) except Exception: pool = None # If pool is None, needs admin also if not pool and not self._user.is_admin: raise AccessDenied() # Check permission for pool.. - if not permissions.checkPermissions(self._user, typing.cast('Model', pool), permissions.PERMISSION_READ): + if not permissions.checkPermissions( + self._user, typing.cast('Model', pool), permissions.PERMISSION_READ + ): raise AccessDenied() if self._args[0] == 'stats': if self._args[1] == 'assigned': @@ -160,9 +166,15 @@ class System(Handler): return getServicesPoolsCounters(pool, counters.CT_CACHED) elif self._args[1] == 'complete': return { - 'assigned': getServicesPoolsCounters(pool, counters.CT_ASSIGNED, since_days=7), - 'inuse': getServicesPoolsCounters(pool, counters.CT_INUSE, since_days=7), - 'cached': getServicesPoolsCounters(pool, counters.CT_CACHED, since_days=7), + 'assigned': getServicesPoolsCounters( + pool, counters.CT_ASSIGNED, since_days=7 + ), + 'inuse': getServicesPoolsCounters( + pool, counters.CT_INUSE, since_days=7 + ), + 'cached': getServicesPoolsCounters( + pool, counters.CT_CACHED, since_days=7 + ), } raise RequestError('invalid request') diff --git a/server/src/uds/REST/methods/tunnel_token.py b/server/src/uds/REST/methods/tunnel_token.py index 9812ac036..590343446 100644 --- a/server/src/uds/REST/methods/tunnel_token.py +++ b/server/src/uds/REST/methods/tunnel_token.py @@ -42,6 +42,7 @@ from uds.core.util import permissions logger = logging.getLogger(__name__) + class TunnelTokens(ModelHandler): model = TunnelToken @@ -62,7 +63,7 @@ class TunnelTokens(ModelHandler): 'username': item.username, 'ip': item.ip, 'hostname': item.hostname, - 'token': item.token + 'token': item.token, } def delete(self) -> str: @@ -72,7 +73,9 @@ class TunnelTokens(ModelHandler): if len(self._args) != 1: raise RequestError('Delete need one and only one argument') - self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to delete + self.ensureAccess( + self.model(), permissions.PERMISSION_ALL, root=True + ) # Must have write permissions to delete try: self.model.objects.get(token=self._args[0]).delete() diff --git a/server/src/uds/REST/methods/user_services.py b/server/src/uds/REST/methods/user_services.py index ea21637f3..ee237fa9b 100644 --- a/server/src/uds/REST/methods/user_services.py +++ b/server/src/uds/REST/methods/user_services.py @@ -51,6 +51,7 @@ class AssignedService(DetailHandler): """ Rest handler for Assigned Services, wich parent is Service """ + custom_methods = [ 'reset', ] @@ -239,7 +240,10 @@ class CachedService(AssignedService): """ Rest handler for Cached Services, wich parent is Service """ - custom_methods: typing.ClassVar[typing.List[str]] = [] # Remove custom methods from assigned services + + custom_methods: typing.ClassVar[ + typing.List[str] + ] = [] # Remove custom methods from assigned services def getItems(self, parent: models.ServicePool, item: typing.Optional[str]): # Extract provider diff --git a/server/src/uds/REST/methods/users_groups.py b/server/src/uds/REST/methods/users_groups.py index 62b38facf..ee0ee58e7 100644 --- a/server/src/uds/REST/methods/users_groups.py +++ b/server/src/uds/REST/methods/users_groups.py @@ -92,15 +92,49 @@ class Users(DetailHandler): # Extract authenticator try: if item is None: - values = list(Users.uuid_to_id(parent.users.all().values('uuid', 'name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin', 'last_access', 'parent'))) + values = list( + Users.uuid_to_id( + parent.users.all().values( + 'uuid', + 'name', + 'real_name', + 'comments', + 'state', + 'staff_member', + 'is_admin', + 'last_access', + 'parent', + ) + ) + ) for res in values: - res['role'] = res['staff_member'] and (res['is_admin'] and _('Admin') or _('Staff member')) or _('User') + res['role'] = ( + res['staff_member'] + and (res['is_admin'] and _('Admin') or _('Staff member')) + or _('User') + ) return values else: u = parent.users.get(uuid=processUuid(item)) - res = model_to_dict(u, fields=('name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin', 'last_access', 'parent')) + res = model_to_dict( + u, + fields=( + 'name', + 'real_name', + 'comments', + 'state', + 'staff_member', + 'is_admin', + 'last_access', + 'parent', + ), + ) res['id'] = u.uuid - res['role'] = res['staff_member'] and (res['is_admin'] and _('Admin') or _('Staff member')) or _('User') + res['role'] = ( + res['staff_member'] + and (res['is_admin'] and _('Admin') or _('Staff member')) + or _('User') + ) usr = aUser(u) res['groups'] = [g.dbGroup().uuid for g in usr.groups()] logger.debug('Item: %s', res) @@ -111,17 +145,34 @@ class Users(DetailHandler): def getTitle(self, parent): try: - return _('Users of {0}').format(Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name) + return _('Users of {0}').format( + Authenticator.objects.get( + uuid=processUuid(self._kwargs['parent_id']) + ).name + ) except Exception: return _('Current users') def getFields(self, parent): return [ - {'name': {'title': _('Username'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-user text-success'}}, + { + 'name': { + 'title': _('Username'), + 'visible': True, + 'type': 'icon', + 'icon': 'fa fa-user text-success', + } + }, {'role': {'title': _('Role')}}, {'real_name': {'title': _('Name')}}, {'comments': {'title': _('Comments')}}, - {'state': {'title': _('state'), 'type': 'dict', 'dict': State.dictionary()}}, + { + 'state': { + 'title': _('state'), + 'type': 'dict', + 'dict': State.dictionary(), + } + }, {'last_access': {'title': _('Last access'), 'type': 'datetime'}}, ] @@ -139,7 +190,14 @@ class Users(DetailHandler): def saveItem(self, parent, item): logger.debug('Saving user %s / %s', parent, item) - valid_fields = ['name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin'] + valid_fields = [ + 'name', + 'real_name', + 'comments', + 'state', + 'staff_member', + 'is_admin', + ] if 'password' in self._params: valid_fields.append('password') self._params['password'] = cryptoManager().hash(self._params['password']) @@ -153,7 +211,9 @@ class Users(DetailHandler): try: auth = parent.getInstance() if item is None: # Create new - auth.createUser(fields) # this throws an exception if there is an error (for example, this auth can't create users) + auth.createUser( + fields + ) # this throws an exception if there is an error (for example, this auth can't create users) user = parent.users.create(**fields) else: auth.modifyUser(fields) # Notifies authenticator @@ -161,7 +221,9 @@ class Users(DetailHandler): user.__dict__.update(fields) logger.debug('User parent: %s', user.parent) - if auth.isExternalSource is False and (user.parent is None or user.parent == ''): + if auth.isExternalSource is False and ( + user.parent is None or user.parent == '' + ): groups = self.readFieldsFromParams(['groups'])['groups'] logger.debug('Groups: %s', groups) logger.debug('Got Groups %s', parent.groups.filter(uuid__in=groups)) @@ -188,7 +250,9 @@ class Users(DetailHandler): user = parent.users.get(uuid=processUuid(item)) if not self._user.is_admin and (user.is_admin or user.staff_member): logger.warn('Removal of user {} denied due to insufficients rights') - raise self.invalidItemException('Removal of user {} denied due to insufficients rights') + raise self.invalidItemException( + 'Removal of user {} denied due to insufficients rights' + ) assignedUserService: 'UserService' for assignedUserService in user.userServices.all(): @@ -216,13 +280,19 @@ class Users(DetailHandler): res = [] groups = list(user.getGroups()) for i in getPoolsForGroups(groups): - res.append({ - 'id': i.uuid, - 'name': i.name, - 'thumb': i.image.thumb64 if i.image is not None else DEFAULT_THUMB_BASE64, - 'user_services_count': i.userServices.exclude(state__in=(State.REMOVED, State.ERROR)).count(), - 'state': _('With errors') if i.isRestrained() else _('Ok'), - }) + res.append( + { + 'id': i.uuid, + 'name': i.name, + 'thumb': i.image.thumb64 + if i.image is not None + else DEFAULT_THUMB_BASE64, + 'user_services_count': i.userServices.exclude( + state__in=(State.REMOVED, State.ERROR) + ).count(), + 'state': _('With errors') if i.isRestrained() else _('Ok'), + } + ) return res @@ -261,19 +331,25 @@ class Groups(DetailHandler): 'comments': i.comments, 'state': i.state, 'type': i.is_meta and 'meta' or 'group', - 'meta_if_any': i.meta_if_any + 'meta_if_any': i.meta_if_any, } if i.is_meta: - val['groups'] = list(x.uuid for x in i.groups.all().order_by('name')) + val['groups'] = list( + x.uuid for x in i.groups.all().order_by('name') + ) res.append(val) if multi or not i: return res # Add pools field if 1 item only res = res[0] if i.is_meta: - res['pools'] = [] # Meta groups do not have "assigned "pools, they get it from groups interaction + res[ + 'pools' + ] = ( + [] + ) # Meta groups do not have "assigned "pools, they get it from groups interaction else: - res['pools'] = [v.uuid for v in i.deployedServices.all()] + res['pools'] = [v.uuid for v in i.deployedServices.all()] return res except Exception: logger.exception('REST groups') @@ -281,15 +357,35 @@ class Groups(DetailHandler): def getTitle(self, parent): try: - return _('Groups of {0}').format(Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name) + return _('Groups of {0}').format( + Authenticator.objects.get( + uuid=processUuid(self._kwargs['parent_id']) + ).name + ) except Exception: return _('Current groups') def getFields(self, parent): return [ - {'name': {'title': _('Group'), 'visible': True, 'type': 'icon_dict', 'icon_dict': {'group': 'fa fa-group text-success', 'meta': 'fa fa-gears text-info'}}}, + { + 'name': { + 'title': _('Group'), + 'visible': True, + 'type': 'icon_dict', + 'icon_dict': { + 'group': 'fa fa-group text-success', + 'meta': 'fa fa-gears text-info', + }, + } + }, {'comments': {'title': _('Comments')}}, - {'state': {'title': _('state'), 'type': 'dict', 'dict': State.dictionary()}}, + { + 'state': { + 'title': _('state'), + 'type': 'dict', + 'dict': State.dictionary(), + } + }, ] def getTypes(self, parent, forType): @@ -297,12 +393,15 @@ class Groups(DetailHandler): 'group': {'name': _('Group'), 'description': _('UDS Group')}, 'meta': {'name': _('Meta group'), 'description': _('UDS Meta Group')}, } - types = [{ - 'name': tDct[t]['name'], - 'type': t, - 'description': tDct[t]['description'], - 'icon': '' - } for t in tDct] + types = [ + { + 'name': tDct[t]['name'], + 'type': t, + 'description': tDct[t]['description'], + 'icon': '', + } + for t in tDct + ] if forType is None: return types @@ -327,7 +426,9 @@ class Groups(DetailHandler): auth = parent.getInstance() if item is None: # Create new if not is_meta and not is_pattern: - auth.createGroup(fields) # this throws an exception if there is an error (for example, this auth can't create groups) + auth.createGroup( + fields + ) # this throws an exception if there is an error (for example, this auth can't create groups) toSave = {} for k in valid_fields: toSave[k] = fields[k] @@ -383,13 +484,19 @@ class Groups(DetailHandler): group = parent.groups.get(uuid=processUuid(uuid)) res = [] for i in getPoolsForGroups((group,)): - res.append({ - 'id': i.uuid, - 'name': i.name, - 'thumb': i.image.thumb64 if i.image is not None else DEFAULT_THUMB_BASE64, - 'user_services_count': i.userServices.exclude(state__in=(State.REMOVED, State.ERROR)).count(), - 'state': _('With errors') if i.isRestrained() else _('Ok'), - }) + res.append( + { + 'id': i.uuid, + 'name': i.name, + 'thumb': i.image.thumb64 + if i.image is not None + else DEFAULT_THUMB_BASE64, + 'user_services_count': i.userServices.exclude( + state__in=(State.REMOVED, State.ERROR) + ).count(), + 'state': _('With errors') if i.isRestrained() else _('Ok'), + } + ) return res @@ -403,7 +510,7 @@ class Groups(DetailHandler): 'name': user.name, 'real_name': user.real_name, 'state': user.state, - 'last_access': user.last_access + 'last_access': user.last_access, } res = [] diff --git a/server/src/uds/REST/methods/version.py b/server/src/uds/REST/methods/version.py index 091df3b8d..ff6727b96 100644 --- a/server/src/uds/REST/methods/version.py +++ b/server/src/uds/REST/methods/version.py @@ -37,12 +37,10 @@ from ..handlers import Handler logger = logging.getLogger(__name__) + class UDSVersion(Handler): authenticated = False # Version requests are public name = 'version' def get(self) -> typing.MutableMapping[str, typing.Any]: - return { - 'version': VERSION, - 'build': VERSION_STAMP - } + return {'version': VERSION, 'build': VERSION_STAMP} diff --git a/server/src/uds/REST/model.py b/server/src/uds/REST/model.py index dcb7ecc66..14e923893 100644 --- a/server/src/uds/REST/model.py +++ b/server/src/uds/REST/model.py @@ -1066,7 +1066,11 @@ class ModelHandler(BaseModelHandler): if tags: logger.debug('Updating tags: %s', tags) item.tags.set( - [Tag.objects.get_or_create(tag=val)[0] for val in tags if val != ''] + [ + Tag.objects.get_or_create(tag=val)[0] + for val in tags + if val != '' + ] ) elif isinstance( tags, list diff --git a/server/src/uds/REST/processors.py b/server/src/uds/REST/processors.py index edf4f5ed9..7bf65bf33 100644 --- a/server/src/uds/REST/processors.py +++ b/server/src/uds/REST/processors.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2019 Virtual Cable S.L. +# Copyright (c) 2012-2021 Virtual Cable S.L.U. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -12,7 +12,7 @@ # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -52,6 +52,7 @@ class ContentProcessor: """ Process contents (request/response) so Handlers can manage them """ + mime_type: typing.ClassVar[str] = '' extensions: typing.ClassVar[typing.Iterable[str]] = [] @@ -81,7 +82,9 @@ class ContentProcessor: Converts an obj to a response of specific type (json, XML, ...) This is done using "render" method of specific type """ - return http.HttpResponse(content=self.render(obj), content_type=self.mime_type + "; charset=utf-8") + return http.HttpResponse( + content=self.render(obj), content_type=self.mime_type + "; charset=utf-8" + ) def render(self, obj: typing.Any): """ @@ -98,7 +101,7 @@ class ContentProcessor: return obj if isinstance(obj, dict): - return {k:ContentProcessor.procesForRender(v) for k, v in obj.items()} + return {k: ContentProcessor.procesForRender(v) for k, v in obj.items()} if isinstance(obj, (list, tuple, types.GeneratorType)): return [ContentProcessor.procesForRender(v) for v in obj] @@ -117,11 +120,15 @@ class MarshallerProcessor(ContentProcessor): If we have a simple marshaller for processing contents this class will allow us to set up a new one simply setting "marshaller" """ + marshaller: typing.ClassVar[typing.Any] = None def processParameters(self) -> typing.MutableMapping[str, typing.Any]: try: - if self._request.META.get('CONTENT_LENGTH', '0') == '0' or not self._request.body: + if ( + self._request.META.get('CONTENT_LENGTH', '0') == '0' + or not self._request.body + ): return self.processGetParameters() # logger.debug('Body: >>{}<< {}'.format(self._request.body, len(self._request.body))) res = self.marshaller.loads(self._request.body.decode('utf8')) @@ -143,14 +150,16 @@ class JsonProcessor(MarshallerProcessor): """ Provides JSON content processor """ + mime_type = 'application/json' extensions = ['json'] marshaller = json # type: ignore + # --------------- # XML Processor # --------------- -#=============================================================================== +# =============================================================================== # class XMLProcessor(MarshallerProcessor): # """ # Provides XML content processor @@ -158,12 +167,14 @@ class JsonProcessor(MarshallerProcessor): # mime_type = 'application/xml' # extensions = ['xml'] # marshaller = xml_marshaller -#=============================================================================== +# =============================================================================== processors_list = (JsonProcessor,) 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_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: diff --git a/server/src/uds/reports/__init__.py b/server/src/uds/reports/__init__.py index 3dd996c04..07c339792 100644 --- a/server/src/uds/reports/__init__.py +++ b/server/src/uds/reports/__init__.py @@ -51,6 +51,7 @@ logger = logging.getLogger(__name__) availableReports: typing.List[typing.Type['reports.Report']] = [] + def __init__() -> None: """ This imports all packages that are descendant of this package, and, after that, @@ -66,7 +67,10 @@ def __init__() -> None: alreadyAdded.add(reportClass.uuid) addReportCls(reportClass) else: - logger.debug('Report class %s not added because it lacks of uuid (it is probably a base class)', reportClass) + logger.debug( + 'Report class %s not added because it lacks of uuid (it is probably a base class)', + reportClass, + ) subReport: typing.Type[reports.Report] for subReport in reportClass.__subclasses__(): diff --git a/server/src/uds/reports/auto/__init__.py b/server/src/uds/reports/auto/__init__.py index 497c454d2..e2212fc62 100644 --- a/server/src/uds/reports/auto/__init__.py +++ b/server/src/uds/reports/auto/__init__.py @@ -122,7 +122,7 @@ class ReportAuto(Report, metaclass=ReportAutoType): # Fills datasource fields.source_field_data(self.getModel(), self.data_source, self.source) - def getModelItems(self) -> typing.Iterable[ReportAutoModel]: # type: ignore + def getModelItems(self) -> typing.Iterable[ReportAutoModel]: model = self.getModel() filters = ( diff --git a/server/src/uds/reports/auto/fields.py b/server/src/uds/reports/auto/fields.py index b0cf449dd..3be659d77 100644 --- a/server/src/uds/reports/auto/fields.py +++ b/server/src/uds/reports/auto/fields.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2020 Virtual Cable S.L. +# Copyright (c) 2020-2021 Virtual Cable S.L.U. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -12,7 +12,7 @@ # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -41,22 +41,24 @@ from uds import models logger = logging.getLogger(__name__) -def start_date_field(order:int) -> gui.DateField: + +def start_date_field(order: int) -> gui.DateField: return gui.DateField( order=order, label=_('Starting date'), tooltip=_('Starting date for report'), defvalue=datetime.date.min, - required=True + required=True, ) + def single_date_field(order: int) -> gui.DateField: - return gui.DateField( + return gui.DateField( order=order, label=_('Date'), tooltip=_('Date for report'), defvalue=datetime.date.today(), - required=True + required=True, ) @@ -66,9 +68,10 @@ def end_date_field(order: int) -> gui.DateField: label=_('Ending date'), tooltip=_('ending date for report'), defvalue=datetime.date.max, - required=True + required=True, ) + def intervals_field(order: int) -> gui.ChoiceField: return gui.ChoiceField( label=_('Report data interval'), @@ -77,14 +80,17 @@ def intervals_field(order: int) -> gui.ChoiceField: 'hour': _('Hourly'), 'day': _('Daily'), 'week': _('Weekly'), - 'month': _('Monthly') + 'month': _('Monthly'), }, tooltip=_('Interval for report data'), required=True, - defvalue='day' + defvalue='day', ) -def source_field(order: int, data_source: str, multiple: bool) -> typing.Union[gui.ChoiceField, gui.MultiChoiceField, None]: + +def source_field( + order: int, data_source: str, multiple: bool +) -> typing.Union[gui.ChoiceField, gui.MultiChoiceField, None]: if not data_source: return None @@ -102,16 +108,18 @@ def source_field(order: int, data_source: str, multiple: bool) -> typing.Union[g logger.debug('Labels: %s, %s', labels, fieldType) - return fieldType( - label=labels[0], - order=order, - tooltip=labels[1], - required=True - ) + return fieldType(label=labels[0], order=order, tooltip=labels[1], required=True) -def source_field_data(model: typing.Any, data_source: str, field: typing.Union[gui.ChoiceField, gui.MultiChoiceField]) -> None: - dataList: typing.List[gui.ChoiceType] = [{'id': x.uuid, 'text': x.name} for x in model.objects.all().order_by('name')] +def source_field_data( + model: typing.Any, + data_source: str, + field: typing.Union[gui.ChoiceField, gui.MultiChoiceField], +) -> None: + + dataList: typing.List[gui.ChoiceType] = [ + {'id': x.uuid, 'text': x.name} for x in model.objects.all().order_by('name') + ] if isinstance(field, gui.MultiChoiceField): dataList.insert(0, {'id': '0-0-0-0', 'text': _('All')}) diff --git a/server/src/uds/reports/lists/base.py b/server/src/uds/reports/lists/base.py index 822d6b837..0ed429e29 100644 --- a/server/src/uds/reports/lists/base.py +++ b/server/src/uds/reports/lists/base.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2015-2019 Virtual Cable S.L. +# Copyright (c) 2015-2021 Virtual Cable S.L.U. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -12,7 +12,7 @@ # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # @@ -35,9 +35,9 @@ import typing from django.utils.translation import ugettext_noop as _ from uds.core import reports + class ListReport(reports.Report): group = _('Lists') # So we can make submenus with reports def generate(self) -> bytes: raise NotImplementedError('ListReport generate invoked and not implemented') - diff --git a/server/src/uds/reports/lists/users.py b/server/src/uds/reports/lists/users.py index 4efbffcd8..5db1a7c50 100644 --- a/server/src/uds/reports/lists/users.py +++ b/server/src/uds/reports/lists/users.py @@ -61,7 +61,7 @@ class ListReportUsers(ListReport): label=_("Authenticator"), order=1, tooltip=_('Authenticator from where to list users'), - required=True + required=True, ) name = _('Users list') # Report name @@ -70,9 +70,7 @@ class ListReportUsers(ListReport): def initGui(self) -> None: logger.debug('Initializing gui') - vals = [ - gui.choiceItem(v.uuid, v.name) for v in Authenticator.objects.all() - ] + vals = [gui.choiceItem(v.uuid, v.name) for v in Authenticator.objects.all()] self.authenticator.setValues(vals) @@ -87,7 +85,7 @@ class ListReportUsers(ListReport): 'auth': auth.name, }, header=ugettext('Users List for {}').format(auth.name), - water=ugettext('UDS Report of users in {}').format(auth.name) + water=ugettext('UDS Report of users in {}').format(auth.name), ) @@ -111,7 +109,9 @@ class ListReportsUsersCSV(ListReportUsers): auth = Authenticator.objects.get(uuid=self.authenticator.value) users = auth.users.order_by('name') - writer.writerow([ugettext('User ID'), ugettext('Real Name'), ugettext('Last access')]) + writer.writerow( + [ugettext('User ID'), ugettext('Real Name'), ugettext('Last access')] + ) for v in users: writer.writerow([v.name, v.real_name, v.last_access]) @@ -120,6 +120,7 @@ class ListReportsUsersCSV(ListReportUsers): return output.getvalue().encode() + # Sample XLSX report # Just for sampling purposses, not used... # class ListReportsUsersXlsx(ListReportUsers): diff --git a/server/src/uds/reports/stats/auth_stats.py b/server/src/uds/reports/stats/auth_stats.py index ce857069d..0dde377fe 100644 --- a/server/src/uds/reports/stats/auth_stats.py +++ b/server/src/uds/reports/stats/auth_stats.py @@ -29,9 +29,6 @@ """ .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com """ -import io -import csv -import datetime import logging import typing @@ -42,10 +39,15 @@ from uds.core.util.stats import counters from .base import StatsReportAuto +if typing.TYPE_CHECKING: + from uds import models + + logger = logging.getLogger(__name__) MAX_ELEMENTS = 10000 + class AuthenticatorsStats(StatsReportAuto): dates = 'range' intervals = True @@ -54,7 +56,9 @@ class AuthenticatorsStats(StatsReportAuto): filename = 'auths_stats.pdf' name = _('Statistics by authenticator') # Report name - description = _('Generates a report with the statistics of an authenticator for a desired period') # Report description + description = _( + 'Generates a report with the statistics of an authenticator for a desired period' + ) # Report description uuid = 'a5a43bc0-d543-11ea-af8f-af01fa65994e' def generate(self) -> typing.Any: @@ -69,9 +73,34 @@ class AuthenticatorsStats(StatsReportAuto): services = 0 userServices = 0 - servicesCounterIter = iter(counters.getCounters(a, counters.CT_AUTH_SERVICES, since=since, interval=interval, limit=MAX_ELEMENTS, use_max=True)) - usersWithServicesCounterIter = iter(counters.getCounters(a, counters.CT_AUTH_USERS_WITH_SERVICES, since=since, interval=interval, limit=MAX_ELEMENTS, use_max=True)) - for userCounter in counters.getCounters(a, counters.CT_AUTH_USERS, since=since, interval=interval, limit=MAX_ELEMENTS, use_max=True): + servicesCounterIter = iter( + counters.getCounters( + typing.cast('models.Authenticator', a), + counters.CT_AUTH_SERVICES, + since=since, + interval=interval, + limit=MAX_ELEMENTS, + use_max=True, + ) + ) + usersWithServicesCounterIter = iter( + counters.getCounters( + typing.cast('models.Authenticator', a), + counters.CT_AUTH_USERS_WITH_SERVICES, + since=since, + interval=interval, + limit=MAX_ELEMENTS, + use_max=True, + ) + ) + for userCounter in counters.getCounters( + typing.cast('models.Authenticator', a), + counters.CT_AUTH_USERS, + since=since, + interval=interval, + limit=MAX_ELEMENTS, + use_max=True, + ): try: while True: servicesCounter = next(servicesCounterIter) @@ -92,18 +121,18 @@ class AuthenticatorsStats(StatsReportAuto): except StopIteration: pass - stats.append({ - 'date': userCounter[0], - 'users': userCounter[1] or 0, - 'services': services, - 'user_services': userServices - }) + stats.append( + { + 'date': userCounter[0], + 'users': userCounter[1] or 0, + 'services': services, + 'user_services': userServices, + } + ) logger.debug('Report Data Done') return self.templateAsPDF( 'uds/reports/stats/authenticator_stats.html', - dct={ - 'data': stats - }, + dct={'data': stats}, header=ugettext('Users usage list'), - water=ugettext('UDS Report of users usage') + water=ugettext('UDS Report of users usage'), ) diff --git a/server/src/uds/reports/stats/base.py b/server/src/uds/reports/stats/base.py index 52c9e0a6d..4cde6ffe7 100644 --- a/server/src/uds/reports/stats/base.py +++ b/server/src/uds/reports/stats/base.py @@ -36,6 +36,7 @@ from django.utils.translation import ugettext_noop as _ from uds.core import reports from ..auto import ReportAuto + class StatsReport(reports.Report): group = _('Statistics') # So we can make submenus with reports diff --git a/server/src/uds/reports/stats/pool_users_summary.py b/server/src/uds/reports/stats/pool_users_summary.py index 45f22562b..f67c5eddb 100644 --- a/server/src/uds/reports/stats/pool_users_summary.py +++ b/server/src/uds/reports/stats/pool_users_summary.py @@ -50,15 +50,14 @@ logger = logging.getLogger(__name__) class UsageSummaryByUsersPool(StatsReport): filename = 'pool_user_usage.pdf' name = _('Pool Usage by users') # Report name - description = _('Generates a report with the summary of users usage for a pool') # Report description + description = _( + 'Generates a report with the summary of users usage for a pool' + ) # Report description uuid = '202c6438-30a8-11e7-80e4-77c1e4cb9e09' # Input fields pool = gui.ChoiceField( - order=1, - label=_('Pool'), - tooltip=_('Pool for report'), - required=True + order=1, label=_('Pool'), tooltip=_('Pool for report'), required=True ) startDate = gui.DateField( @@ -66,7 +65,7 @@ class UsageSummaryByUsersPool(StatsReport): label=_('Starting date'), tooltip=_('starting date for report'), defvalue=datetime.date.min, - required=True + required=True, ) endDate = gui.DateField( @@ -74,22 +73,32 @@ class UsageSummaryByUsersPool(StatsReport): label=_('Finish date'), tooltip=_('finish date for report'), defvalue=datetime.date.max, - required=True + required=True, ) def initGui(self) -> None: logger.debug('Initializing gui') - vals = [ - gui.choiceItem(v.uuid, v.name) for v in ServicePool.objects.all() - ] + vals = [gui.choiceItem(v.uuid, v.name) for v in ServicePool.objects.all()] self.pool.setValues(vals) - def getPoolData(self, pool) -> typing.Tuple[typing.List[typing.Dict[str, typing.Any]], str]: + def getPoolData( + self, pool + ) -> typing.Tuple[typing.List[typing.Dict[str, typing.Any]], str]: start = self.startDate.stamp() end = self.endDate.stamp() logger.debug(self.pool.value) - items = StatsManager.manager().getEvents(events.OT_DEPLOYED, (events.ET_LOGIN, events.ET_LOGOUT), owner_id=pool.id, since=start, to=end).order_by('stamp') + items = ( + StatsManager.manager() + .getEvents( + events.OT_DEPLOYED, + (events.ET_LOGIN, events.ET_LOGOUT), + owner_id=pool.id, + since=start, + to=end, + ) + .order_by('stamp') + ) logins: typing.Dict[str, int] = {} users: typing.Dict[str, typing.Dict] = {} @@ -115,12 +124,15 @@ class UsageSummaryByUsersPool(StatsReport): # }) # Extract different number of users - data = [{ - 'user': k, - 'sessions': v['sessions'], - 'hours': '{:.2f}'.format(float(v['time']) / 3600), - 'average': '{:.2f}'.format(float(v['time']) / 3600 / v['sessions']) - } for k, v in users.items()] + data = [ + { + 'user': k, + 'sessions': v['sessions'], + 'hours': '{:.2f}'.format(float(v['time']) / 3600), + 'average': '{:.2f}'.format(float(v['time']) / 3600 / v['sessions']), + } + for k, v in users.items() + ] return data, pool.name @@ -139,7 +151,7 @@ class UsageSummaryByUsersPool(StatsReport): 'ending': self.endDate.date(), }, header=ugettext('Users usage list for {}').format(poolName), - water=ugettext('UDS Report of users in {}').format(poolName) + water=ugettext('UDS Report of users in {}').format(poolName), ) @@ -160,7 +172,14 @@ class UsageSummaryByUsersPoolCSV(UsageSummaryByUsersPool): reportData = self.getData()[0] - writer.writerow([ugettext('User'), ugettext('Sessions'), ugettext('Hours'), ugettext('Average')]) + writer.writerow( + [ + ugettext('User'), + ugettext('Sessions'), + ugettext('Hours'), + ugettext('Average'), + ] + ) for v in reportData: writer.writerow([v['user'], v['sessions'], v['hours'], v['average']])