diff --git a/server/src/uds/REST/handlers.py b/server/src/uds/REST/handlers.py index aca5ab7d1..be28a7e5a 100644 --- a/server/src/uds/REST/handlers.py +++ b/server/src/uds/REST/handlers.py @@ -46,7 +46,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 ExtendedHttpRequest + from uds.core.util.request import ExtendedHttpRequestWithUser logger = logging.getLogger(__name__) @@ -100,7 +100,7 @@ class Handler: 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: 'ExtendedHttpRequest' # It's a modified HttpRequest + _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 .... @@ -113,7 +113,7 @@ class Handler: # method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace' - def __init__(self, request: 'ExtendedHttpRequest', 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 diff --git a/server/src/uds/REST/methods/actor_v3.py b/server/src/uds/REST/methods/actor_v3.py index 9d54c81ef..8f06f9152 100644 --- a/server/src/uds/REST/methods/actor_v3.py +++ b/server/src/uds/REST/methods/actor_v3.py @@ -240,11 +240,12 @@ class Initialize(ActorV3Action): """ # First, validate token... logger.debug('Args: %s, Params: %s', self._args, self._params) + service: typing.Optional[Service] = None try: # First, try to locate an user service providing this token. if self._params['type'] == UNMANAGED: # If unmanaged, use Service locator - service: Service = Service.objects.get(token=self._params['token']) + service = Service.objects.get(token=self._params['token']) # Locate an userService that belongs to this service and which # Build the possible ids and make initial filter to match service idsList = [x['ip'] for x in self._params['id']] + [ @@ -262,6 +263,30 @@ class Initialize(ActorV3Action): # Valid actor token, now validate access allowed. That is, look for a valid mac from the ones provided. try: + # Set full filter + dbFilter = dbFilter.filter( + unique_id__in=idsList, + state__in=[State.USABLE, State.PREPARING], + ) + + # If no UserService exists, + # ist managed (service exists), then it's a "local login" + if dbFilter.exists() is False and service: + # The userService does not exists, try to lock the id on the service + serviceInstance = service.getInstance() + lockedId = serviceInstance.lockId(idsList) + if lockedId: + # Return an "generic" result allowing login/logout processing + return ActorV3Action.actorResult( + { + 'own_token': self._params['token'], + 'unique_id': lockedId, + 'os': None, + } + ) + else: # if no lock, return empty result + raise Exception() # Unmanaged host + userService: UserService = next( iter( dbFilter.filter( @@ -463,6 +488,16 @@ class Login(LoginLogout): name = 'login' + # payload received + # { + # 'type': actor_type or types.MANAGED, + # 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces], + # 'token': token, + # 'username': username, + # 'session_type': sessionType, + # 'secret': secret or '', + # } + @staticmethod def process_login( userService: UserService, username: str diff --git a/server/src/uds/REST/methods/client.py b/server/src/uds/REST/methods/client.py index b6a4f6b81..e7cc5c041 100644 --- a/server/src/uds/REST/methods/client.py +++ b/server/src/uds/REST/methods/client.py @@ -38,7 +38,7 @@ from django.utils.translation import ugettext as _ from django.urls import reverse from uds.REST import Handler from uds.REST import RequestError -from uds.models import TicketStore, user +from uds.models import TicketStore from uds.models import User from uds.web.util import errors from uds.core.managers import cryptoManager, userServiceManager @@ -166,9 +166,14 @@ class Client(Handler): '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) except Exception as e: logger.exception("Exception") return Client.result(error=str(e)) + diff --git a/server/src/uds/REST/methods/connection.py b/server/src/uds/REST/methods/connection.py index 61c446d01..d1bf9c3f2 100644 --- a/server/src/uds/REST/methods/connection.py +++ b/server/src/uds/REST/methods/connection.py @@ -88,7 +88,7 @@ class Connection(Handler): # Ensure user is present on request, used by web views methods self._request.user = self._user - return Connection.result(result=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] @@ -152,7 +152,7 @@ class Connection(Handler): self._request.ip, hostname ) # Store where we are accessing from so we can notify Service - if not ip: + if not ip or not transportInstance: raise ServiceNotReadyError() transportScript = transportInstance.getEncodedTransportScript( diff --git a/server/src/uds/core/auths/auth.py b/server/src/uds/core/auths/auth.py index ef0bc51cd..b0f01498e 100644 --- a/server/src/uds/core/auths/auth.py +++ b/server/src/uds/core/auths/auth.py @@ -345,7 +345,7 @@ def authInfoUrl(authenticator: typing.Union[str, bytes, Authenticator]) -> str: def webLogin( - request: 'ExtendedHttpRequest', response: HttpResponse, user: User, password: str + request: 'ExtendedHttpRequest', response: typing.Optional[HttpResponse], user: User, password: str ) -> bool: """ Helper function to, once the user is authenticated, store the information at the user session. diff --git a/server/src/uds/core/auths/authenticator.py b/server/src/uds/core/auths/authenticator.py index 6e89f07b6..1c66763a3 100644 --- a/server/src/uds/core/auths/authenticator.py +++ b/server/src/uds/core/auths/authenticator.py @@ -491,7 +491,7 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods """ return None - def getInfo(self, parameters: typing.Dict[str, str]) -> typing.Optional[typing.Tuple[str, typing.Optional[str]]]: + def getInfo(self, parameters: typing.Mapping[str, str]) -> typing.Optional[typing.Tuple[str, typing.Optional[str]]]: """ This method is invoked whenever the authinfo url is invoked, with the name of the authenticator If this is implemented, information returned by this will be shown via web. diff --git a/server/src/uds/core/managers/user_service.py b/server/src/uds/core/managers/user_service.py index 86787df6e..6950881d0 100644 --- a/server/src/uds/core/managers/user_service.py +++ b/server/src/uds/core/managers/user_service.py @@ -736,7 +736,7 @@ class UserServiceManager: os: typing.MutableMapping, srcIp: str, idService: str, - idTransport: str, + idTransport: typing.Optional[str], doTest: bool = True, clientHostname: typing.Optional[str] = None, ) -> typing.Tuple[ @@ -750,7 +750,7 @@ class UserServiceManager: Get service info from user service """ if idService[0] == 'M': # Meta pool - return self.getMeta(user, srcIp, os, idService[1:], idTransport) + return self.getMeta(user, srcIp, os, idService[1:], idTransport or '') userService = self.locateUserService(user, idService, create=True) @@ -900,7 +900,7 @@ class UserServiceManager: ip, ) raise ServiceNotReadyError( - code=serviceNotReadyCode, service=userService, transport=transport + code=serviceNotReadyCode, userService=userService, transport=transport ) def getMeta( diff --git a/server/src/uds/core/services/exceptions.py b/server/src/uds/core/services/exceptions.py index bbff82190..2b61c336c 100644 --- a/server/src/uds/core/services/exceptions.py +++ b/server/src/uds/core/services/exceptions.py @@ -102,10 +102,10 @@ class ServiceNotReadyError(ServiceException): Can include an optional code error """ code: int - service: 'UserService' - transport: 'Transport' + userService: typing.Optional['UserService'] + transport: typing.Optional['Transport'] def __init__(self, *args, **kwargs): - super(ServiceNotReadyError, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.code = kwargs.get('code', 0x0000) - self.service = kwargs.get('service', None) + self.userService = kwargs.get('service', None) self.transport = kwargs.get('transport', None) diff --git a/server/src/uds/core/services/service.py b/server/src/uds/core/services/service.py index 866575e9e..cb4488dbe 100644 --- a/server/src/uds/core/services/service.py +++ b/server/src/uds/core/services/service.py @@ -303,17 +303,17 @@ class Service(Module): """ return None - def lockId(self, id: str) -> bool: + def lockId(self, id: typing.List[str]) -> typing.Optional[str]: """ Locks the id, so it cannot be used by a service pool. Args: - id (str): Id to lock + id (typing.List[str]): Id to lock (list of possible ids) Returns: - bool: True if the id has been locked, False if not + str: Valid id of locked element, or None if no id found """ - return False + return None def processLogin(self, id: str, remote_login: bool) -> None: """ diff --git a/server/src/uds/models/user.py b/server/src/uds/models/user.py index b6e630684..76fafcb14 100644 --- a/server/src/uds/models/user.py +++ b/server/src/uds/models/user.py @@ -88,9 +88,11 @@ class User(UUIDModel): ordering = ('name',) app_label = 'uds' - # unique_together = (("manager", "name"),) + # unique_together = (("manager", "name"),) constraints = [ - models.UniqueConstraint(fields=['manager', 'name'], name='u_usr_manager_name') + models.UniqueConstraint( + fields=['manager', 'name'], name='u_usr_manager_name' + ) ] def getUsernameForAuth(self) -> str: diff --git a/server/src/uds/models/user_preference.py b/server/src/uds/models/user_preference.py index e45210a90..cfec755f4 100644 --- a/server/src/uds/models/user_preference.py +++ b/server/src/uds/models/user_preference.py @@ -44,6 +44,7 @@ class UserPreference(models.Model): """ This class represents a single user preference for an user and a module """ + module = models.CharField(max_length=32, db_index=True) name = models.CharField(max_length=32, db_index=True) value = models.CharField(max_length=128, db_index=True) @@ -56,4 +57,6 @@ class UserPreference(models.Model): app_label = 'uds' def __str__(self) -> str: - return '{}.{} = "{}" for user {}'.format(self.module, self.name, self.value, self.user) + return '{}.{} = "{}" for user {}'.format( + self.module, self.name, self.value, self.user + ) diff --git a/server/src/uds/web/views/auth.py b/server/src/uds/web/views/auth.py index 509ade045..13ce85a5a 100644 --- a/server/src/uds/web/views/auth.py +++ b/server/src/uds/web/views/auth.py @@ -32,7 +32,7 @@ import logging import typing from django.urls import reverse -from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.utils.translation import ugettext as _ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt @@ -49,6 +49,9 @@ from uds.core.util.model import processUuid from uds.models import Authenticator, ServicePool from uds.models import TicketStore +if typing.TYPE_CHECKING: + from uds.core.util.request import ExtendedHttpRequestWithUser + logger = logging.getLogger(__name__) # The callback is now a two stage, so we can use cookies samesite policy to "Lax" @@ -83,7 +86,7 @@ def authCallback(request: HttpRequest, authName: str) -> HttpResponse: return errors.exceptionView(request, e) -def authCallback_stage2(request: HttpRequest, ticketId: str) -> HttpResponse: +def authCallback_stage2(request: ExtendedHttpRequestWithUser, ticketId: str) -> HttpResponse: try: ticket = TicketStore.get(ticketId) params: typing.Dict[str, typing.Any] = ticket['params'] @@ -134,7 +137,7 @@ def authInfo(request: 'HttpRequest', authName: str) -> HttpResponse: logger.debug('Getting info for %s', authName) authenticator = Authenticator.objects.get(name=authName) authInstance = authenticator.getInstance() - if authInstance.getInfo == auths.Authenticator.getInfo: + if typing.cast(typing.Any, authInstance.getInfo) == auths.Authenticator.getInfo: raise Exception() # This authenticator do not provides info info = authInstance.getInfo(request.GET) @@ -170,7 +173,7 @@ def customAuth(request: 'HttpRequest', idAuth: str) -> HttpResponse: @never_cache -def ticketAuth(request: 'HttpRequest', ticketId: str) -> HttpResponse: # pylint: disable=too-many-locals,too-many-branches,too-many-statements +def ticketAuth(request: 'ExtendedHttpRequestWithUser', ticketId: str) -> HttpResponse: # pylint: disable=too-many-locals,too-many-branches,too-many-statements """ Used to authenticate an user via a ticket """ @@ -209,7 +212,7 @@ def ticketAuth(request: 'HttpRequest', ticketId: str) -> HttpResponse: # pylint raise auths.exceptions.InvalidUserException() # Add groups to user (replace existing groups) - usr.groups.set(grps) + usr.groups.set(grps) # type: ignore # Force cookie generation webLogin(request, None, usr, password)