From 29ff0081a403f134f3534f0fe9db2ac3a54c59e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Fri, 3 Nov 2023 18:47:20 +0100 Subject: [PATCH] Refactorin exceptions, types, constants, ... --- server/src/uds/REST/methods/login_logout.py | 72 ++++++++++-------- server/src/uds/REST/methods/services.py | 2 +- server/src/uds/REST/methods/users_groups.py | 50 ++++--------- server/src/uds/REST/model.py | 2 +- server/src/uds/auths/IP/authenticator.py | 12 +-- .../src/uds/auths/InternalDB/authenticator.py | 12 +-- server/src/uds/auths/OAuth2/authenticator.py | 48 ++++++------ server/src/uds/auths/Radius/authenticator.py | 8 +- .../src/uds/auths/RegexLdap/authenticator.py | 69 ++++++----------- server/src/uds/auths/SAML/saml.py | 56 +++++++------- server/src/uds/auths/Sample/SampleAuth.py | 35 +++------ .../src/uds/auths/SimpleLDAP/authenticator.py | 75 ++++++------------- server/src/uds/core/auths/__init__.py | 6 -- server/src/uds/core/auths/auth.py | 22 +++--- server/src/uds/core/auths/authenticator.py | 54 +++---------- server/src/uds/core/consts/auth.py | 1 - server/src/uds/core/exceptions/__init__.py | 39 ++++++++++ server/src/uds/core/exceptions/actor.py | 54 +++++++++++++ .../exceptions.py => exceptions/auth.py} | 4 +- .../{exceptions.py => exceptions/common.py} | 25 ------- server/src/uds/core/exceptions/service.py | 40 ++++++++++ server/src/uds/core/exceptions/validation.py | 39 ++++++++++ server/src/uds/core/managers/crypto.py | 14 +++- server/src/uds/core/managers/downloads.py | 29 ++++--- server/src/uds/core/mfas/mfa.py | 10 +-- server/src/uds/core/types/auth.py | 39 ++++++++++ server/src/uds/core/ui/user_interface.py | 4 +- server/src/uds/core/util/auth.py | 2 +- server/src/uds/core/util/validators.py | 58 +++++++------- server/src/uds/mfas/Email/mfa.py | 2 +- server/src/uds/mfas/Radius/mfa.py | 5 +- server/src/uds/mfas/TOTP/mfa.py | 8 +- server/src/uds/models/user.py | 4 +- server/src/uds/notifiers/email/notifier.py | 2 +- server/src/uds/notifiers/telegram/notifier.py | 2 +- .../LinuxOsManager/linux_ad_osmanager.py | 6 +- .../LinuxOsManager/linux_freeipa_osmanager.py | 6 +- .../linux_randompass_osmanager.py | 2 +- .../osmanagers/WindowsOsManager/windows.py | 4 +- .../WindowsOsManager/windows_domain.py | 8 +- .../WindowsOsManager/windows_random.py | 4 +- server/src/uds/services/OVirt/service.py | 2 +- .../uds/services/PhysicalMachines/provider.py | 8 +- .../PhysicalMachines/service_multi.py | 2 +- .../PhysicalMachines/service_single.py | 2 +- server/src/uds/services/Sample/provider.py | 4 +- server/src/uds/services/Sample/service.py | 2 +- server/src/uds/services/Xen/service.py | 2 +- server/src/uds/transports/SPICE/spice.py | 2 +- .../src/uds/transports/SPICE/spicetunnel.py | 2 +- server/src/uds/transports/Test/transport.py | 2 +- server/src/uds/transports/URL/url_custom.py | 2 +- server/src/uds/urls.py | 4 +- server/src/uds/web/views/main.py | 8 +- 54 files changed, 526 insertions(+), 450 deletions(-) create mode 100644 server/src/uds/core/exceptions/__init__.py create mode 100644 server/src/uds/core/exceptions/actor.py rename server/src/uds/core/{auths/exceptions.py => exceptions/auth.py} (97%) rename server/src/uds/core/{exceptions.py => exceptions/common.py} (77%) create mode 100644 server/src/uds/core/exceptions/service.py create mode 100644 server/src/uds/core/exceptions/validation.py diff --git a/server/src/uds/REST/methods/login_logout.py b/server/src/uds/REST/methods/login_logout.py index d6223ed26..c522a609b 100644 --- a/server/src/uds/REST/methods/login_logout.py +++ b/server/src/uds/REST/methods/login_logout.py @@ -112,34 +112,50 @@ class Login(Handler): raise AccessDenied('Too many fails') try: - if ( - 'auth_id' not in self._params - and 'authId' not in self._params - and 'auth_id' not in self._params - and 'authSmallName' not in self._params - and 'authLabel' not in self._params - and 'auth_label' not in self._params - and 'auth' not in self._params + # if ( + # 'auth_id' not in self._params + # and 'authId' not in self._params + # and 'auth_id' not in self._params + # and 'authSmallName' not in self._params + # and 'authLabel' not in self._params + # and 'auth_label' not in self._params + # and 'auth' not in self._params + # ): + # raise RequestError('Invalid parameters (no auth)') + + # Check if we have a valid auth + if not any( + i in self._params + for i in ('auth_id', 'authId', 'authSmallName', 'authLabel', 'auth_label', 'auth') ): raise RequestError('Invalid parameters (no auth)') - scrambler: str = ''.join( - random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(32) + authId: typing.Optional[str] = self._params.get( + 'auth_id', + self._params.get('authId', None), # Old compat, alias ) - authId: typing.Optional[str] = self._params.get('authId', self._params.get('auth_id', None)) authLabel: typing.Optional[str] = self._params.get( 'auth_label', - self._params.get('authSmallName', None), + self._params.get( + 'authSmallName', # Old compat name + self._params.get('authLabel', None), # Old compat name + ), ) authName: typing.Optional[str] = self._params.get('auth', None) platform: str = self._params.get('platform', self._request.os.os.value[0]) - username: str - password: str - - username, password = self._params['username'], self._params['password'] + username: str = self._params['username'] + password: str = self._params['password'] locale: str = self._params.get('locale', 'en') - if authName == 'admin' or authLabel == 'admin' or authId == '00000000-0000-0000-0000-000000000000': + + # Generate a random scrambler + scrambler: str = CryptoManager.manager().randomString(32) + if ( + authName == 'admin' + or authLabel == 'admin' + or authId == '00000000-0000-0000-0000-000000000000' + or (not authId and not authName and not authLabel) + ): if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and CryptoManager().checkHash( password, GlobalConfig.SUPER_USER_PASS.get(True) ): @@ -147,13 +163,6 @@ class Login(Handler): return Login.result(result='ok', token=self.getAuthToken()) return Login.result(error='Invalid credentials') - # invalid login - if ( - functools.reduce(lambda a, b: (a << 4) + b, [i for i in username.encode()]) - == 474216907296766572900491101513 - ): - return Login.result(result=bytes([i ^ 64 for i in b'\x13(%+(`-!`3()%2!+)`!..)']).decode()) - # Will raise an exception if no auth found if authId: auth = Authenticator.objects.get(uuid=processUuid(authId)) @@ -162,8 +171,8 @@ class Login(Handler): else: auth = Authenticator.objects.get(small_name=authLabel) - if not password: - password = 'xdaf44tgas4xd5ñasdłe4g€@#½|«ð2' # nosec: Extrange password if credential left empty. Value is not important, just not empty + # No matter in fact the password, just not empty (so it can be encrypted, but will be invalid anyway) + password = password or CryptoManager().randomString(32) logger.debug('Auth obj: %s', auth) authResult = authenticate(username, password, auth, self._request, True) @@ -218,19 +227,22 @@ class Auths(Handler): authenticated = False # By default, all handlers needs authentication def auths(self) -> typing.Iterable[typing.Dict[str, typing.Any]]: - paramAll: bool = self._params.get('all', 'false') == 'true' + paramAll: bool = self._params.get('all', 'false').lower() == 'true' auth: Authenticator for auth in Authenticator.objects.all(): theType = auth.getType() if paramAll or (theType.isCustom() is False and theType.typeType not in ('IP',)): yield { - 'authId': auth.uuid, + 'authId': auth.uuid, # Deprecated, use 'auth_id' + 'auth_id': auth.uuid, 'authSmallName': str(auth.small_name), # Deprecated - 'authLabel': str(auth.small_name), + 'authLabel': str(auth.small_name), # Deprecated, use 'auth_label' + 'auth_label': str(auth.small_name), 'auth': auth.name, 'type': theType.typeType, 'priority': auth.priority, - 'isCustom': theType.isCustom(), + 'isCustom': theType.isCustom(), # Deprecated, use 'custom' + 'custom': theType.isCustom(), } def get(self) -> typing.List[typing.Dict[str, typing.Any]]: diff --git a/server/src/uds/REST/methods/services.py b/server/src/uds/REST/methods/services.py index 6c098c3a3..e16f7f7d4 100644 --- a/server/src/uds/REST/methods/services.py +++ b/server/src/uds/REST/methods/services.py @@ -200,7 +200,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods ) ) from e raise RequestError(_('Element already exists (duplicate key error)')) from e - except exceptions.ValidationError as e: + except exceptions.validation.ValidationError as e: if ( not item and service ): # Only remove partially saved element if creating new (if editing, ignore this) diff --git a/server/src/uds/REST/methods/users_groups.py b/server/src/uds/REST/methods/users_groups.py index e95fcb2ee..92a773073 100644 --- a/server/src/uds/REST/methods/users_groups.py +++ b/server/src/uds/REST/methods/users_groups.py @@ -39,14 +39,13 @@ from django.core.exceptions import ValidationError from uds.core.util.state import State -from uds.core.auths.exceptions import AuthenticatorException from uds.core.auths.user import User as aUser from uds.core.util import log, ensure from uds.core.util.model import processUuid from uds.models import Authenticator, User, Group, ServicePool from uds.core.managers.crypto import CryptoManager from uds.REST import RequestError -from uds.core.consts.images import DEFAULT_THUMB_BASE64 +from uds.core import consts, exceptions from uds.REST.model import DetailHandler @@ -84,10 +83,9 @@ class Users(DetailHandler): def getItems(self, parent: 'Model', item: typing.Optional[str]) -> typing.Any: parent = ensure.is_instance(parent, Authenticator) + # processes item to change uuid key for id - def uuid_to_id( - iterable: typing.Iterable[typing.Any] # will get values from a queryset - ): + def uuid_to_id(iterable: typing.Iterable[typing.Any]): # will get values from a queryset for v in iterable: v['id'] = v['uuid'] del v['uuid'] @@ -140,9 +138,7 @@ class Users(DetailHandler): ) res['id'] = u.uuid res['role'] = ( - res['staff_member'] - and (res['is_admin'] and _('Admin') or _('Staff member')) - or _('User') + 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()] @@ -155,9 +151,7 @@ class Users(DetailHandler): def getTitle(self, parent: 'Model') -> str: try: return _('Users of {0}').format( - Authenticator.objects.get( - uuid=processUuid(self._kwargs['parent_id']) - ).name + Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name ) except Exception: return _('Current users') @@ -245,16 +239,12 @@ class Users(DetailHandler): if not auth.isExternalSource and not user.parent: groups = self.readFieldsFromParams(['groups'])['groups'] # Save but skip meta groups, they are not real groups, but just a way to group users based on rules - user.groups.set( - g - for g in parent.groups.filter(uuid__in=groups) - if g.is_meta is False - ) + user.groups.set(g for g in parent.groups.filter(uuid__in=groups) if g.is_meta is False) except User.DoesNotExist: raise self.invalidItemException() from None except IntegrityError: # Duplicate key probably raise RequestError(_('User already exists (duplicate key error)')) from None - except AuthenticatorException as e: + except exceptions.auth.AuthenticatorException as e: raise RequestError(str(e)) from e except ValidationError as e: raise RequestError(str(e.message)) from e @@ -310,9 +300,7 @@ class Users(DetailHandler): { 'id': i.uuid, 'name': i.name, - 'thumb': i.image.thumb64 - if i.image is not None - else DEFAULT_THUMB_BASE64, + 'thumb': i.image.thumb64 if i.image is not None else consts.images.DEFAULT_THUMB_BASE64, 'user_services_count': i.userServices.exclude( state__in=(State.REMOVED, State.ERROR) ).count(), @@ -367,9 +355,7 @@ class Groups(DetailHandler): '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: return res @@ -475,9 +461,7 @@ class Groups(DetailHandler): if is_meta: # Do not allow to add meta groups to meta groups group.groups.set( - i - for i in parent.groups.filter(uuid__in=self._params['groups']) - if i.is_meta is False + i for i in parent.groups.filter(uuid__in=self._params['groups']) if i.is_meta is False ) if pools: @@ -489,7 +473,7 @@ class Groups(DetailHandler): raise self.invalidItemException() from None except IntegrityError: # Duplicate key probably raise RequestError(_('User already exists (duplicate key error)')) from None - except AuthenticatorException as e: + except exceptions.auth.AuthenticatorException as e: raise RequestError(str(e)) from e except RequestError: # pylint: disable=try-except-raise raise # Re-raise @@ -506,9 +490,7 @@ class Groups(DetailHandler): except Exception: raise self.invalidItemException() from None - def servicesPools( - self, parent: 'Model', item: str - ) -> typing.List[typing.Mapping[str, typing.Any]]: + def servicesPools(self, parent: 'Model', item: str) -> typing.List[typing.Mapping[str, typing.Any]]: parent = ensure.is_instance(parent, Authenticator) uuid = processUuid(item) group = parent.groups.get(uuid=processUuid(uuid)) @@ -518,9 +500,7 @@ class Groups(DetailHandler): { 'id': i.uuid, 'name': i.name, - 'thumb': i.image.thumb64 - if i.image is not None - else DEFAULT_THUMB_BASE64, + 'thumb': i.image.thumb64 if i.image is not None else consts.images.DEFAULT_THUMB_BASE64, 'user_services_count': i.userServices.exclude( state__in=(State.REMOVED, State.ERROR) ).count(), @@ -530,9 +510,7 @@ class Groups(DetailHandler): return res - def users( - self, parent: 'Model', item: str - ) -> typing.List[typing.Mapping[str, typing.Any]]: + def users(self, parent: 'Model', item: str) -> typing.List[typing.Mapping[str, typing.Any]]: uuid = processUuid(item) parent = ensure.is_instance(parent, Authenticator) group = parent.groups.get(uuid=processUuid(uuid)) diff --git a/server/src/uds/REST/model.py b/server/src/uds/REST/model.py index 3325d911c..784520e2f 100644 --- a/server/src/uds/REST/model.py +++ b/server/src/uds/REST/model.py @@ -1156,7 +1156,7 @@ class ModelHandler(BaseModelHandler): raise exceptions.NotFound('Item not found') from None except IntegrityError: # Duplicate key probably raise exceptions.RequestError('Element already exists (duplicate key error)') from None - except (exceptions.SaveException, udsExceptions.ValidationError) as e: + except (exceptions.SaveException, udsexceptions.validation.ValidationError) as e: raise exceptions.RequestError(str(e)) from e except (exceptions.RequestError, exceptions.ResponseError): raise diff --git a/server/src/uds/auths/IP/authenticator.py b/server/src/uds/auths/IP/authenticator.py index 90a03240e..4ae3ae63f 100644 --- a/server/src/uds/auths/IP/authenticator.py +++ b/server/src/uds/auths/IP/authenticator.py @@ -104,12 +104,12 @@ class IPAuth(auths.Authenticator): credentials: str, # pylint: disable=unused-argument groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: # If credentials is a dict, that can't be sent directly from web interface, we allow entering if username == self.getIp(request): self.getGroups(username, groupsManager) - return auths.SUCCESS_AUTH - return auths.FAILED_AUTH + return types.auth.SUCCESS_AUTH + return types.auth.FAILED_AUTH def isAccesibleFrom(self, request: 'ExtendedHttpRequest'): """ @@ -127,15 +127,15 @@ class IPAuth(auths.Authenticator): credentials: str, # pylint: disable=unused-argument groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: # In fact, username does not matter, will get IP from request username = self.getIp(request) # Override provided username and use source IP self.getGroups(username, groupsManager) if groupsManager.hasValidGroups() and self.dbObj().isValidUser( username, True ): - return auths.SUCCESS_AUTH - return auths.FAILED_AUTH + return types.auth.SUCCESS_AUTH + return types.auth.FAILED_AUTH @staticmethod def test(env, data): # pylint: disable=unused-argument diff --git a/server/src/uds/auths/InternalDB/authenticator.py b/server/src/uds/auths/InternalDB/authenticator.py index c9f53adcc..d3f10f8f0 100644 --- a/server/src/uds/auths/InternalDB/authenticator.py +++ b/server/src/uds/auths/InternalDB/authenticator.py @@ -39,7 +39,7 @@ import dns.resolver import dns.reversename from django.utils.translation import gettext_noop as _ -from uds.core import auths, types +from uds.core import auths, types, exceptions, consts from uds.core.auths.auth import authLogLogin from uds.core.managers.crypto import CryptoManager from uds.core.ui import gui @@ -147,25 +147,25 @@ class InternalDBAuth(auths.Authenticator): credentials: str, groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: username = username.lower() dbAuth = self.dbObj() try: user: 'models.User' = dbAuth.users.get(name=username, state=State.ACTIVE) except Exception: authLogLogin(request, self.dbObj(), username, 'Invalid user') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH if user.parent: # Direct auth not allowed for "derived" users - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Internal Db Auth has its own groups. (That is, no external source). If a group is active it is valid if CryptoManager().checkHash(credentials, user.password): groupsManager.validate([g.name for g in user.groups.all()]) - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH authLogLogin(request, self.dbObj(), username, 'Invalid password') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'): dbAuth = self.dbObj() diff --git a/server/src/uds/auths/OAuth2/authenticator.py b/server/src/uds/auths/OAuth2/authenticator.py index 3bd904e4b..d634e1d17 100644 --- a/server/src/uds/auths/OAuth2/authenticator.py +++ b/server/src/uds/auths/OAuth2/authenticator.py @@ -375,7 +375,7 @@ class OAuth2Authenticator(auths.Authenticator): def _processToken( self, userInfo: typing.Mapping[str, typing.Any], gm: 'auths.GroupsManager' - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: # After this point, we don't mind about the token, we only need to authenticate user # and get some basic info from it @@ -398,11 +398,11 @@ class OAuth2Authenticator(auths.Authenticator): # We don't mind about the token, we only need to authenticate user # and if we are here, the user is authenticated, so we can return SUCCESS_AUTH - return auths.AuthenticationResult(auths.AuthenticationState.SUCCESS, username=username) + return types.auth.AuthenticationResult(types.auth.AuthenticationState.SUCCESS, username=username) def _processTokenOpenId( self, token_id: str, nonce: str, gm: 'auths.GroupsManager' - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: # Get token headers, to extract algorithm info = jwt.get_unverified_header(token_id) logger.debug('Token headers: %s', info) @@ -428,19 +428,19 @@ class OAuth2Authenticator(auths.Authenticator): pass except Exception as e: logger.error('Error decoding token: %s', e) - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # All keys tested, none worked logger.error('Invalid token received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None: if not values: return if ' ' in values['name']: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('This kind of Authenticator does not support white spaces on field NAME') ) @@ -449,9 +449,9 @@ class OAuth2Authenticator(auths.Authenticator): if self.responseType.value in ('code', 'pkce', 'openid+code'): if self.commonGroups.value.strip() == '': - raise exceptions.ValidationError(gettext('Common groups is required for "code" response types')) + raise exceptions.validation.ValidationError(gettext('Common groups is required for "code" response types')) if self.tokenEndpoint.value.strip() == '': - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('Token endpoint is required for "code" response types') ) # infoEndpoint will not be necesary if the response of tokenEndpoint contains the user info @@ -459,7 +459,7 @@ class OAuth2Authenticator(auths.Authenticator): if self.responseType.value == 'openid+token_id': # Ensure we have a public key if self.publicKey.value.strip() == '': - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('Public key is required for "openid+token_id" response type') ) @@ -473,7 +473,7 @@ class OAuth2Authenticator(auths.Authenticator): parameters: 'types.auth.AuthCallbackParams', gm: 'auths.GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: match self.responseType.value: case 'code' | 'pkce': return self.authCallbackCode(parameters, gm, request) @@ -491,8 +491,8 @@ class OAuth2Authenticator(auths.Authenticator): self, request: 'types.request.ExtendedHttpRequest', # pylint: disable=unused-argument username: str, # pylint: disable=unused-argument - ) -> auths.AuthenticationResult: - return auths.SUCCESS_AUTH + ) -> types.auth.AuthenticationResult: + return types.auth.SUCCESS_AUTH def getJavascript(self, request: 'HttpRequest') -> typing.Optional[str]: """ @@ -517,7 +517,7 @@ class OAuth2Authenticator(auths.Authenticator): parameters: 'types.auth.AuthCallbackParams', gm: 'auths.GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: """Process the callback for code authorization flow""" state = parameters.get_params.get('state', '') # Remove state from cache @@ -526,13 +526,13 @@ class OAuth2Authenticator(auths.Authenticator): if not state or not code_verifier: logger.error('Invalid state received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Get the code code = parameters.get_params.get('code', '') if code == '': logger.error('Invalid code received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Restore code_verifier "none" to None if code_verifier == 'none': @@ -546,7 +546,7 @@ class OAuth2Authenticator(auths.Authenticator): parameters: 'types.auth.AuthCallbackParams', gm: 'auths.GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: """Process the callback for PKCE authorization flow""" state = parameters.get_params.get('state', '') stateValue = self.cache.get(state) @@ -554,7 +554,7 @@ class OAuth2Authenticator(auths.Authenticator): if not state or not stateValue: logger.error('Invalid state received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Get the token, token_type, expires token = TokenInfo( @@ -574,7 +574,7 @@ class OAuth2Authenticator(auths.Authenticator): parameters: 'types.auth.AuthCallbackParams', gm: 'auths.GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: """Process the callback for OpenID authorization flow""" state = parameters.post_params.get('state', '') nonce = self.cache.get(state) @@ -582,20 +582,20 @@ class OAuth2Authenticator(auths.Authenticator): if not state or not nonce: logger.error('Invalid state received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Get the code code = parameters.post_params.get('code', '') if code == '': logger.error('Invalid code received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Get the token, token_type, expires token = self._requestToken(code) if not token.id_token: logger.error('No id_token received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH return self._processTokenOpenId(token.id_token, nonce, gm) @@ -604,7 +604,7 @@ class OAuth2Authenticator(auths.Authenticator): parameters: 'types.auth.AuthCallbackParams', gm: 'auths.GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: """Process the callback for OpenID authorization flow""" state = parameters.post_params.get('state', '') nonce = self.cache.get(state) @@ -612,12 +612,12 @@ class OAuth2Authenticator(auths.Authenticator): if not state or not nonce: logger.error('Invalid state received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH # Get the id_token id_token = parameters.post_params.get('id_token', '') if id_token == '': logger.error('Invalid id_token received on OAuth2 callback') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH return self._processTokenOpenId(id_token, nonce, gm) diff --git a/server/src/uds/auths/Radius/authenticator.py b/server/src/uds/auths/Radius/authenticator.py index 64cd92c2f..98b6b29e2 100644 --- a/server/src/uds/auths/Radius/authenticator.py +++ b/server/src/uds/auths/Radius/authenticator.py @@ -35,7 +35,7 @@ import typing from django.utils.translation import gettext_noop as _ -from uds.core import auths, types +from uds.core import auths, types, consts from uds.core.auths.auth import authLogLogin from uds.core.managers.crypto import CryptoManager from uds.core.ui import gui @@ -146,7 +146,7 @@ class RadiusAuth(auths.Authenticator): credentials: str, groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: try: connection = self.radiusClient() groups, mfaCode, state = connection.authenticate(username=username, password=credentials, mfaField=self.mfaAttr.value.strip()) @@ -167,7 +167,7 @@ class RadiusAuth(auths.Authenticator): username, 'Access denied by Raiuds', ) - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH if self.globalGroup.value.strip(): groups.append(self.globalGroup.value.strip()) @@ -179,7 +179,7 @@ class RadiusAuth(auths.Authenticator): # Validate groups groupsManager.validate(groups) - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH def getGroups(self, username: str, groupsManager: 'auths.GroupsManager') -> None: with self.storage.map() as storage: diff --git a/server/src/uds/auths/RegexLdap/authenticator.py b/server/src/uds/auths/RegexLdap/authenticator.py index 0b03f6e05..17490b2e5 100644 --- a/server/src/uds/auths/RegexLdap/authenticator.py +++ b/server/src/uds/auths/RegexLdap/authenticator.py @@ -38,7 +38,7 @@ import typing import ldap from django.utils.translation import gettext_noop as _ -from uds.core import auths, exceptions, types +from uds.core import auths, exceptions, types, consts from uds.core.auths.auth import authLogLogin from uds.core.ui import gui from uds.core.util import ldaputil, auth as auth_utils @@ -61,7 +61,6 @@ LDAP_RESULT_LIMIT = 100 class RegexLdap(auths.Authenticator): - host = gui.TextField( length=64, label=_('Host'), @@ -80,9 +79,7 @@ class RegexLdap(auths.Authenticator): ssl = gui.CheckBoxField( label=_('Use SSL'), order=3, - tooltip=_( - 'If checked, the connection will be ssl, using port 636 instead of 389' - ), + tooltip=_('If checked, the connection will be ssl, using port 636 instead of 389'), ) username = gui.TextField( length=64, @@ -113,9 +110,7 @@ class RegexLdap(auths.Authenticator): label=_('Verify SSL'), default=True, order=11, - tooltip=_( - 'If checked, SSL verification will be enforced. If not, SSL verification will be disabled' - ), + tooltip=_('If checked, SSL verification will be enforced. If not, SSL verification will be disabled'), tab=types.ui.Tab.ADVANCED, ) certificate = gui.TextField( @@ -184,9 +179,7 @@ class RegexLdap(auths.Authenticator): label=_('Alt. class'), default='', order=25, - tooltip=_( - 'Class for LDAP objects that will be also checked for groups retrieval (normally empty)' - ), + tooltip=_('Class for LDAP objects that will be also checked for groups retrieval (normally empty)'), required=False, tab=_('Advanced'), ) @@ -327,9 +320,9 @@ class RegexLdap(auths.Authenticator): def unmarshal(self, data: bytes) -> None: vals = data.decode('utf8').split('\t') - self._verifySsl = False # Backward compatibility - self._mfaAttr = '' # Backward compatibility - self._certificate = '' # Backward compatibility + self._verifySsl = False # Backward compatibility + self._mfaAttr = '' # Backward compatibility + self._certificate = '' # Backward compatibility # Common logger.debug('Common: %s', vals[1:11]) @@ -386,7 +379,7 @@ class RegexLdap(auths.Authenticator): @return: Connection established @raise exception: If connection could not be established """ - if self._connection is None: # If connection is not established, try to connect + if self._connection is None: # If connection is not established, try to connect self._connection = ldaputil.connection( self._username, self._password, @@ -485,7 +478,7 @@ class RegexLdap(auths.Authenticator): credentials: str, groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ Must authenticate the user. We can have to different situations here: @@ -500,21 +493,15 @@ class RegexLdap(auths.Authenticator): usr = self.__getUser(username) if usr is None: - authLogLogin( - request, self.dbObj(), username, 'Invalid user' - ) - return auths.FAILED_AUTH + authLogLogin(request, self.dbObj(), username, 'Invalid user') + return types.auth.FAILED_AUTH try: # Let's see first if it credentials are fine - self.__connectAs( - usr['dn'], credentials - ) # Will raise an exception if it can't connect + self.__connectAs(usr['dn'], credentials) # Will raise an exception if it can't connect except Exception: - authLogLogin( - request, self.dbObj(), username, 'Invalid password' - ) - return auths.FAILED_AUTH + authLogLogin(request, self.dbObj(), username, 'Invalid password') + return types.auth.FAILED_AUTH # store the user mfa attribute if it is set if self._mfaAttr: @@ -525,10 +512,10 @@ class RegexLdap(auths.Authenticator): groupsManager.validate(self.__getGroups(usr)) - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH except Exception: - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH def createUser(self, usrData: typing.Dict[str, str]) -> None: """ @@ -541,7 +528,7 @@ class RegexLdap(auths.Authenticator): """ res = self.__getUser(usrData['name']) if res is None: - raise auths.exceptions.AuthenticatorException(_('Username not found')) + raise exceptions.auth.AuthenticatorException(_('Username not found')) # Fills back realName field usrData['real_name'] = self.__getUserRealName(res) @@ -572,7 +559,7 @@ class RegexLdap(auths.Authenticator): """ user = self.__getUser(username) if user is None: - raise auths.exceptions.AuthenticatorException(_('Username not found')) + raise exceptions.auth.AuthenticatorException(_('Username not found')) groups = self.__getGroups(user) groupsManager.validate(groups) @@ -597,9 +584,7 @@ class RegexLdap(auths.Authenticator): return res except Exception as e: logger.exception("Exception: ") - raise auths.exceptions.AuthenticatorException( - _('Too many results, be more specific') - ) from e + raise exceptions.auth.AuthenticatorException(_('Too many results, be more specific')) from e @staticmethod def test(env, data): @@ -607,9 +592,7 @@ class RegexLdap(auths.Authenticator): auth = RegexLdap(None, env, data) # type: ignore # Regexldap does not use "dbAuth", so it's safe... return auth.testConnection() except Exception as e: - logger.error( - 'Exception found testing Simple LDAP auth %s: %s', e.__class__, e - ) + logger.error('Exception found testing Simple LDAP auth %s: %s', e.__class__, e) return [False, "Error testing connection"] def testConnection(self): @@ -638,9 +621,7 @@ class RegexLdap(auths.Authenticator): raise Exception() return [ False, - _( - 'Ldap user class seems to be incorrect (no user found by that class)' - ), + _('Ldap user class seems to be incorrect (no user found by that class)'), ] except Exception: # nosec: Control flow # If found 1 or more, all right @@ -662,9 +643,7 @@ class RegexLdap(auths.Authenticator): raise Exception() return [ False, - _( - 'Ldap user id attr is probably wrong (can\'t find any user with both conditions)' - ), + _('Ldap user id attr is probably wrong (can\'t find any user with both conditions)'), ] except Exception: # nosec: Control flow # If found 1 or more, all right @@ -691,9 +670,7 @@ class RegexLdap(auths.Authenticator): continue return [ False, - _( - 'Ldap group id attribute seems to be incorrect (no group found by that attribute)' - ), + _('Ldap group id attribute seems to be incorrect (no group found by that attribute)'), ] # Now try to test regular expression to see if it matches anything ( diff --git a/server/src/uds/auths/SAML/saml.py b/server/src/uds/auths/SAML/saml.py index 93581f033..00dca1043 100644 --- a/server/src/uds/auths/SAML/saml.py +++ b/server/src/uds/auths/SAML/saml.py @@ -44,7 +44,7 @@ from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser from onelogin.saml2.settings import OneLogin_Saml2_Settings -from uds.core import auths, exceptions, types +from uds.core import auths, exceptions, types, consts from uds.core.managers.crypto import CryptoManager from uds.core.types.request import ExtendedHttpRequest from uds.core.ui import gui @@ -335,7 +335,7 @@ class SAMLAuthenticator(auths.Authenticator): return if ' ' in values['name']: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('This kind of Authenticator does not support white spaces on field NAME') ) @@ -344,7 +344,7 @@ class SAMLAuthenticator(auths.Authenticator): # This is in fact not needed, but we may say something useful to user if we check this if self.serverCertificate.value.startswith('-----BEGIN CERTIFICATE-----\n') is False: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext( 'Server certificate should be a valid PEM (PEM certificates starts with -----BEGIN CERTIFICATE-----)' ) @@ -353,13 +353,13 @@ class SAMLAuthenticator(auths.Authenticator): try: CryptoManager().loadCertificate(self.serverCertificate.value) except Exception as e: - raise exceptions.ValidationError(gettext('Invalid server certificate. ') + str(e)) + raise exceptions.validation.ValidationError(gettext('Invalid server certificate. ') + str(e)) if ( self.privateKey.value.startswith('-----BEGIN RSA PRIVATE KEY-----\n') is False and self.privateKey.value.startswith('-----BEGIN PRIVATE KEY-----\n') is False ): - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext( 'Private key should be a valid PEM (PEM private keys starts with -----BEGIN RSA PRIVATE KEY-----' ) @@ -368,12 +368,12 @@ class SAMLAuthenticator(auths.Authenticator): try: CryptoManager().loadPrivateKey(self.privateKey.value) except Exception as e: - raise exceptions.ValidationError(gettext('Invalid private key. ') + str(e)) + raise exceptions.validation.ValidationError(gettext('Invalid private key. ') + str(e)) if not security.checkCertificateMatchPrivateKey( cert=self.serverCertificate.value, key=self.privateKey.value ): - raise exceptions.ValidationError(gettext('Certificate and private key do not match')) + raise exceptions.validation.ValidationError(gettext('Certificate and private key do not match')) request: 'ExtendedHttpRequest' = values['_request'] @@ -394,7 +394,7 @@ class SAMLAuthenticator(auths.Authenticator): ) idpMetadata = resp.content.decode() except Exception as e: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('Can\'t fetch url {0}: {1}').format(self.idpMetadata.value, str(e)) ) fromUrl = True @@ -405,7 +405,7 @@ class SAMLAuthenticator(auths.Authenticator): xml.sax.parseString(idpMetadata, xml.sax.ContentHandler()) # type: ignore # nosec: url provided by admin except Exception as e: msg = (gettext(' (obtained from URL)') if fromUrl else '') + str(e) - raise exceptions.ValidationError(gettext('XML does not seem valid for IDP Metadata ') + msg) + raise exceptions.validation.ValidationError(gettext('XML does not seem valid for IDP Metadata ') + msg) # Now validate regular expressions, if they exists auth_utils.validateRegexField(self.userNameAttr) @@ -469,7 +469,7 @@ class SAMLAuthenticator(auths.Authenticator): val = resp.content.decode() except Exception as e: logger.error('Error fetching idp metadata: %s', e) - raise auths.exceptions.AuthenticatorException(gettext('Can\'t access idp metadata')) + raise exceptions.auth.AuthenticatorException(gettext('Can\'t access idp metadata')) else: val = self.idpMetadata.value @@ -535,7 +535,7 @@ class SAMLAuthenticator(auths.Authenticator): metadata = saml_settings.get_sp_metadata() errors = saml_settings.validate_metadata(metadata) if len(errors) > 0: - raise auths.exceptions.AuthenticatorException( + raise exceptions.auth.AuthenticatorException( gettext('Error validating SP metadata: ') + str(errors) ) if isinstance(metadata, str): @@ -572,7 +572,7 @@ class SAMLAuthenticator(auths.Authenticator): self, req: typing.Dict[str, typing.Any], request: 'ExtendedHttpRequestWithUser', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: # Convert HTTP-POST to HTTP-REDIRECT on SAMLResponse, for just in case... if 'SAMLResponse' in req['post_data']: if isinstance(req['post_data']['SAMLResponse'], list): @@ -596,15 +596,15 @@ class SAMLAuthenticator(auths.Authenticator): logger.debug('Error on SLO: %s', auth.get_last_response_xml()) logger.debug('post_data: %s', req['post_data']) logger.info('Errors processing logout request: %s', errors) - raise auths.exceptions.AuthenticatorException(gettext('Error processing SLO: ') + str(errors)) + raise exceptions.auth.AuthenticatorException(gettext('Error processing SLO: ') + str(errors)) # Remove MFA related data if request.user: self.mfaClean(request.user.name) - return auths.AuthenticationResult( - success=auths.AuthenticationState.REDIRECT, - url=url or auths.AuthenticationInternalUrl.LOGIN.getUrl(), + return types.auth.AuthenticationResult( + success=types.auth.AuthenticationState.REDIRECT, + url=url or types.auth.AuthenticationInternalUrl.LOGIN.getUrl(), ) # pylint: disable=too-many-locals,too-many-branches,too-many-statements @@ -613,7 +613,7 @@ class SAMLAuthenticator(auths.Authenticator): parameters: 'types.auth.AuthCallbackParams', gm: 'auths.GroupsManager', request: 'ExtendedHttpRequestWithUser', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: req = self.getReqFromRequest(request, params=parameters) if 'logout' in parameters.get_params: @@ -624,13 +624,13 @@ class SAMLAuthenticator(auths.Authenticator): auth = OneLogin_Saml2_Auth(req, settings) auth.process_response() except Exception as e: - raise auths.exceptions.AuthenticatorException(gettext('Error processing SAML response: ') + str(e)) + raise exceptions.auth.AuthenticatorException(gettext('Error processing SAML response: ') + str(e)) errors = auth.get_errors() if errors: - raise auths.exceptions.AuthenticatorException('SAML response error: ' + str(errors)) + raise exceptions.auth.AuthenticatorException('SAML response error: ' + str(errors)) if not auth.is_authenticated(): - raise auths.exceptions.AuthenticatorException(gettext('SAML response not authenticated')) + raise exceptions.auth.AuthenticatorException(gettext('SAML response not authenticated')) # Store SAML attributes request.session['SAML'] = { @@ -648,7 +648,7 @@ class SAMLAuthenticator(auths.Authenticator): # 'RelayState' in req['post_data'] # and OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState'] # ): - # return auths.AuthenticationResult( + # return types.auth.AuthenticationResult( # success=auths.AuthenticationSuccess.REDIRECT, # url=auth.redirect_to(req['post_data']['RelayState']) # ) @@ -658,7 +658,7 @@ class SAMLAuthenticator(auths.Authenticator): attributes.update(auth.get_friendlyname_attributes()) if not attributes: - raise auths.exceptions.AuthenticatorException(gettext('No attributes returned from IdP')) + raise exceptions.auth.AuthenticatorException(gettext('No attributes returned from IdP')) logger.debug("Attributes: %s", attributes) # Now that we have attributes, we can extract values from this, map groups, etc... @@ -689,11 +689,11 @@ class SAMLAuthenticator(auths.Authenticator): gm.validate(groups) - return auths.AuthenticationResult(success=auths.AuthenticationState.SUCCESS, username=username) + return types.auth.AuthenticationResult(success=types.auth.AuthenticationState.SUCCESS, username=username) - def logout(self, request: 'ExtendedHttpRequest', username: str) -> auths.AuthenticationResult: + def logout(self, request: 'ExtendedHttpRequest', username: str) -> types.auth.AuthenticationResult: if not self.globalLogout.isTrue(): - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH req = self.getReqFromRequest(request) @@ -710,10 +710,10 @@ class SAMLAuthenticator(auths.Authenticator): self.mfaClean(username) if not saml: - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH - return auths.AuthenticationResult( - success=auths.AuthenticationState.REDIRECT, + return types.auth.AuthenticationResult( + success=types.auth.AuthenticationState.REDIRECT, url=auth.logout( name_id=saml.get('nameid'), session_index=saml.get('session_index'), diff --git a/server/src/uds/auths/Sample/SampleAuth.py b/server/src/uds/auths/Sample/SampleAuth.py index 007a81f15..6c1dba65b 100644 --- a/server/src/uds/auths/Sample/SampleAuth.py +++ b/server/src/uds/auths/Sample/SampleAuth.py @@ -34,10 +34,9 @@ import logging import typing from django.utils.translation import gettext_noop as _ -from uds.core.auths.authenticator import AuthenticationResult, AuthenticationState from uds.core.types.request import ExtendedHttpRequest from uds.core.ui import gui -from uds.core import auths, exceptions +from uds.core import auths, exceptions, types, consts if typing.TYPE_CHECKING: from django.http import ( @@ -119,9 +118,7 @@ class SampleAuth(auths.Authenticator): # : We will define a simple form where we will use a simple # : list editor to allow entering a few group names - groups = gui.EditableListField( - label=_('Groups'), default=['Gods', 'Daemons', 'Mortals'] - ) + groups = gui.EditableListField(label=_('Groups'), default=['Gods', 'Daemons', 'Mortals']) def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None: """ @@ -134,7 +131,7 @@ class SampleAuth(auths.Authenticator): # unserialization, and at this point all will be default values # so self.groups.value will be [] if values and len(self.groups.value) < 2: - raise exceptions.ValidationError(_('We need more than two groups!')) + raise exceptions.validation.ValidationError(_('We need more than two groups!')) def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: """ @@ -175,7 +172,7 @@ class SampleAuth(auths.Authenticator): credentials: str, groupsManager: 'GroupsManager', request: 'ExtendedHttpRequest', # pylint: disable=unused-argument - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ This method is invoked by UDS whenever it needs an user to be authenticated. It is used from web interface, but also from administration interface to @@ -217,10 +214,8 @@ class SampleAuth(auths.Authenticator): :note: groupsManager is an in/out parameter """ - if ( - username != credentials - ): # All users with same username and password are allowed - return auths.FAILED_AUTH + if username != credentials: # All users with same username and password are allowed + return types.auth.FAILED_AUTH # Now the tricky part. We will make this user belong to groups that contains at leat # two letters equals to the groups names known by UDS @@ -230,7 +225,7 @@ class SampleAuth(auths.Authenticator): if len(set(g.lower()).intersection(username.lower())) >= 2: groupsManager.validate(g) - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'): """ @@ -247,9 +242,7 @@ class SampleAuth(auths.Authenticator): if len(set(g.lower()).intersection(username.lower())) >= 2: groupsManager.validate(g) - def getJavascript( - self, request: 'HttpRequest' # pylint: disable=unused-argument - ) -> typing.Optional[str]: + def getJavascript(self, request: 'HttpRequest') -> typing.Optional[str]: # pylint: disable=unused-argument """ If we override this method from the base one, we are telling UDS that we want to draw our own authenticator. @@ -272,20 +265,16 @@ class SampleAuth(auths.Authenticator): # I know, this is a bit ugly, but this is just a sample :-) res = '

Login name:

' - res += ( - '

Login

' return res def authCallback( self, parameters: typing.Dict[str, typing.Any], - gm: 'auths.GroupsManager', # pylint: disable=unused-argument + gm: 'auths.GroupsManager', # pylint: disable=unused-argument request: 'ExtendedHttpRequestWithUser', # pylint: disable=unused-argument - ) -> AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ We provide this as a sample of callback for an user. We will accept all petitions that has "user" parameter @@ -301,7 +290,7 @@ class SampleAuth(auths.Authenticator): """ user = parameters.get('user', None) - return AuthenticationResult(AuthenticationState.SUCCESS, username=user) + return types.auth.AuthenticationResult(types.auth.AuthenticationState.SUCCESS, username=user) def createUser(self, usrData: typing.Dict[str, str]) -> None: """ diff --git a/server/src/uds/auths/SimpleLDAP/authenticator.py b/server/src/uds/auths/SimpleLDAP/authenticator.py index 6ac9980b4..021ffce33 100644 --- a/server/src/uds/auths/SimpleLDAP/authenticator.py +++ b/server/src/uds/auths/SimpleLDAP/authenticator.py @@ -36,7 +36,7 @@ import ldap import ldap.filter from django.utils.translation import gettext_noop as _ -from uds.core import auths, types +from uds.core import auths, types, consts, exceptions from uds.core.auths.auth import authLogLogin from uds.core.ui import gui from uds.core.util import ldaputil @@ -49,6 +49,7 @@ logger = logging.getLogger(__name__) LDAP_RESULT_LIMIT = 100 + # pylint: disable=too-many-instance-attributes class SimpleLDAPAuthenticator(auths.Authenticator): host = gui.TextField( @@ -69,9 +70,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): ssl = gui.CheckBoxField( label=_('Use SSL'), order=3, - tooltip=_( - 'If checked, the connection will be ssl, using port 636 instead of 389' - ), + tooltip=_('If checked, the connection will be ssl, using port 636 instead of 389'), ) username = gui.TextField( length=64, @@ -103,9 +102,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): label=_('Verify SSL'), default=True, order=11, - tooltip=_( - 'If checked, SSL verification will be enforced. If not, SSL verification will be disabled' - ), + tooltip=_('If checked, SSL verification will be enforced. If not, SSL verification will be disabled'), tab=types.ui.Tab.ADVANCED, ) certificate = gui.TextField( @@ -149,9 +146,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): label=_('User Name Attr'), default='uid', order=33, - tooltip=_( - 'Attributes that contains the user name (list of comma separated values)' - ), + tooltip=_('Attributes that contains the user name (list of comma separated values)'), required=True, tab=_('Ldap info'), ) @@ -240,9 +235,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): self._userIdAttr = values['userIdAttr'] self._groupIdAttr = values['groupIdAttr'] self._memberAttr = values['memberAttr'] - self._userNameAttr = values['userNameAttr'].replace( - ' ', '' - ) # Removes white spaces + self._userNameAttr = values['userNameAttr'].replace(' ', '') # Removes white spaces self._mfaAttr = values['mfaAttr'] self._verifySsl = gui.toBool(values['verifySsl']) self._certificate = values['certificate'] @@ -438,7 +431,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): credentials: str, groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest', - ) -> auths.AuthenticationResult: + ) -> types.auth.AuthenticationResult: ''' Must authenticate the user. We can have to different situations here: @@ -454,18 +447,14 @@ class SimpleLDAPAuthenticator(auths.Authenticator): if user is None: authLogLogin(request, self.dbObj(), username, 'Invalid user') - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH try: # Let's see first if it credentials are fine - self.__connectAs( - user['dn'], credentials - ) # Will raise an exception if it can't connect + self.__connectAs(user['dn'], credentials) # Will raise an exception if it can't connect except Exception: - authLogLogin( - request, self.dbObj(), username, 'Invalid password' - ) - return auths.FAILED_AUTH + authLogLogin(request, self.dbObj(), username, 'Invalid password') + return types.auth.FAILED_AUTH # store the user mfa attribute if it is set if self._mfaAttr: @@ -476,10 +465,10 @@ class SimpleLDAPAuthenticator(auths.Authenticator): groupsManager.validate(self.__getGroups(user)) - return auths.SUCCESS_AUTH + return types.auth.SUCCESS_AUTH except Exception: - return auths.FAILED_AUTH + return types.auth.FAILED_AUTH def createUser(self, usrData: typing.Dict[str, str]) -> None: ''' @@ -489,7 +478,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): ''' res = self.__getUser(usrData['name']) if res is None: - raise auths.exceptions.AuthenticatorException(_('Username not found')) + raise exceptions.auth.AuthenticatorException(_('Username not found')) # Fills back realName field usrData['real_name'] = self.__getUserRealName(res) @@ -522,7 +511,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): ''' res = self.__getGroup(groupData['name']) if res is None: - raise auths.exceptions.AuthenticatorException(_('Group not found')) + raise exceptions.auth.AuthenticatorException(_('Group not found')) def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'): ''' @@ -532,7 +521,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): ''' user = self.__getUser(username) if user is None: - raise auths.exceptions.AuthenticatorException(_('Username not found')) + raise exceptions.auth.AuthenticatorException(_('Username not found')) groupsManager.validate(self.__getGroups(user)) def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: @@ -555,9 +544,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): return res except Exception as e: logger.exception("Exception: ") - raise auths.exceptions.AuthenticatorException( - _('Too many results, be more specific') - ) from e + raise exceptions.auth.AuthenticatorException(_('Too many results, be more specific')) from e def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: try: @@ -574,9 +561,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): return res except Exception as e: logger.exception("Exception: ") - raise auths.exceptions.AuthenticatorException( - _('Too many results, be more specific') - ) from e + raise exceptions.auth.AuthenticatorException(_('Too many results, be more specific')) from e @staticmethod def test(env, data) -> typing.List[typing.Any]: @@ -589,9 +574,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): def testConnection( self, - ) -> typing.List[ - typing.Any - ]: # pylint: disable=too-many-return-statements,too-many-branches + ) -> typing.List[typing.Any]: # pylint: disable=too-many-return-statements,too-many-branches try: con = self.__connection() except Exception as e: @@ -617,9 +600,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): raise Exception() return [ False, - _( - 'Ldap user class seems to be incorrect (no user found by that class)' - ), + _('Ldap user class seems to be incorrect (no user found by that class)'), ] except Exception: # nosec: Flow control # If found 1 or more, all right @@ -640,9 +621,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): raise Exception() return [ False, - _( - 'Ldap group class seems to be incorrect (no group found by that class)' - ), + _('Ldap group class seems to be incorrect (no group found by that class)'), ] except Exception: # nosec: Flow control # If found 1 or more, all right @@ -663,9 +642,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): raise Exception() return [ False, - _( - 'Ldap user id attribute seems to be incorrect (no user found by that attribute)' - ), + _('Ldap user id attribute seems to be incorrect (no user found by that attribute)'), ] except Exception: # nosec: Flow control # If found 1 or more, all right @@ -686,9 +663,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): raise Exception() return [ False, - _( - 'Ldap group id attribute seems to be incorrect (no group found by that attribute)' - ), + _('Ldap group id attribute seems to be incorrect (no group found by that attribute)'), ] except Exception: # nosec: Flow control # If found 1 or more, all right @@ -738,9 +713,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator): ok = True break if ok is False: - raise Exception( - _('Can\'t locate any group with the membership attribute specified') - ) + raise Exception(_('Can\'t locate any group with the membership attribute specified')) except Exception as e: return [False, str(e)] diff --git a/server/src/uds/core/auths/__init__.py b/server/src/uds/core/auths/__init__.py index 13c3267a9..a6610fcb1 100644 --- a/server/src/uds/core/auths/__init__.py +++ b/server/src/uds/core/auths/__init__.py @@ -34,17 +34,11 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com """ from .authenticator import ( Authenticator, - AuthenticationResult, - AuthenticationState, - AuthenticationInternalUrl, - SUCCESS_AUTH, - FAILED_AUTH, ) from .authfactory import AuthsFactory from .user import User from .group import Group from .groups_manager import GroupsManager -from . import exceptions def factory() -> AuthsFactory: diff --git a/server/src/uds/core/auths/auth.py b/server/src/uds/core/auths/auth.py index c7a61057a..4bbc0a81d 100644 --- a/server/src/uds/core/auths/auth.py +++ b/server/src/uds/core/auths/auth.py @@ -49,7 +49,7 @@ from django.urls import reverse from django.utils.translation import gettext as _ -from uds.core import auths, types +from uds.core import auths, types, exceptions from uds.core.types.request import ExtendedHttpRequest from uds.core.util import log from uds.core.util import net @@ -58,7 +58,7 @@ from uds.core.util.config import GlobalConfig from uds.core.util.stats import events from uds.core.util.state import State from uds.core.managers.crypto import CryptoManager -from uds.core.auths import Authenticator as AuthenticatorInstance, SUCCESS_AUTH +from uds.core.auths import Authenticator as AuthenticatorInstance from uds import models @@ -309,12 +309,12 @@ def authenticate( else: res = authInstance.internalAuthenticate(username, password, gm, request) - if res.success == auths.AuthenticationState.FAIL: + if res.success == types.auth.AuthenticationState.FAIL: logger.debug('Authentication failed') # Maybe it's an redirection on auth failed? return AuthResult() - if res.success == auths.AuthenticationState.REDIRECT: + if res.success == types.auth.AuthenticationState.REDIRECT: return AuthResult(url=res.url) logger.debug('Groups manager: %s', gm) @@ -358,15 +358,15 @@ def authenticateViaCallback( # If there is no callback for this authenticator... if authInstance.authCallback is auths.Authenticator.authCallback: - raise auths.exceptions.InvalidAuthenticatorException() + raise exceptions.auth.InvalidAuthenticatorException() result = authInstance.authCallback(params, gm, request) - if result.success == auths.AuthenticationState.FAIL or ( - result.success == auths.AuthenticationState.SUCCESS and not gm.hasValidGroups() + if result.success == types.auth.AuthenticationState.FAIL or ( + result.success == types.auth.AuthenticationState.SUCCESS and not gm.hasValidGroups() ): - raise auths.exceptions.InvalidUserException('User doesn\'t has access to UDS') + raise exceptions.auth.InvalidUserException('User doesn\'t has access to UDS') - if result.success == auths.AuthenticationState.REDIRECT: + if result.success == types.auth.AuthenticationState.REDIRECT: return AuthResult(url=result.url) if result.username: @@ -374,7 +374,7 @@ def authenticateViaCallback( else: logger.warning('Authenticator %s returned empty username', authenticator.name) - raise auths.exceptions.InvalidUserException('User doesn\'t has access to UDS') + raise exceptions.auth.InvalidUserException('User doesn\'t has access to UDS') def authCallbackUrl(authenticator: models.Authenticator) -> str: @@ -488,7 +488,7 @@ def webLogout( authenticator = request.user.manager.getInstance() username = request.user.name logout = authenticator.logout(request, username) - if logout and logout.success == auths.AuthenticationState.REDIRECT: + if logout and logout.success == types.auth.AuthenticationState.REDIRECT: exit_url = logout.url or exit_url if request.user.id != ROOT_ID: # Log the event if not root user diff --git a/server/src/uds/core/auths/authenticator.py b/server/src/uds/core/auths/authenticator.py index 4630171b8..ddd691dfc 100644 --- a/server/src/uds/core/auths/authenticator.py +++ b/server/src/uds/core/auths/authenticator.py @@ -32,15 +32,13 @@ Base module for all authenticators Author: Adolfo Gómez, dkmaster at dkmon dot com """ -import enum import logging import typing from django.utils.translation import gettext_noop as _ -from django.urls import reverse from uds.core.module import Module -from uds.core import consts +from uds.core import consts, types # Not imported at runtime, just for type checking if typing.TYPE_CHECKING: @@ -57,40 +55,6 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class AuthenticationState(enum.IntEnum): - """ - Enumeration for authentication success - """ - - FAIL = 0 - SUCCESS = 1 - REDIRECT = 2 - - -class AuthenticationInternalUrl(enum.Enum): - """ - Enumeration for authentication success - """ - - LOGIN = 'page.login' - - def getUrl(self) -> str: - """ - Returns the url for the given internal url - """ - return reverse(self.value) - - -class AuthenticationResult(typing.NamedTuple): - success: AuthenticationState - url: typing.Optional[str] = None - username: typing.Optional[str] = None - - -FAILED_AUTH = AuthenticationResult(success=AuthenticationState.FAIL) -SUCCESS_AUTH = AuthenticationResult(success=AuthenticationState.SUCCESS) - - class Authenticator(Module): """ This class represents the base interface to implement authenticators. @@ -202,7 +166,7 @@ class Authenticator(Module): # : group class groupType: typing.ClassVar[typing.Type[Group]] = Group - _dbObj: typing.Optional['models.Authenticator'] = None # Cached dbAuth object + _dbObj: typing.Optional['models.Authenticator'] = None # Cached dbAuth object def __init__( self, @@ -356,7 +320,7 @@ class Authenticator(Module): credentials: str, groupsManager: 'GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ This method must be overriden, and is responsible for authenticating users. @@ -394,7 +358,7 @@ class Authenticator(Module): This is done in this way, because UDS has only a subset of groups for this user, and we let the authenticator decide inside wich groups of UDS this users is included. """ - return FAILED_AUTH + return types.auth.FAILED_AUTH def isAccesibleFrom(self, request: 'HttpRequest') -> bool: """ @@ -429,7 +393,7 @@ class Authenticator(Module): credentials: str, groupsManager: 'GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ This method is provided so "plugins" (For example, a custom dispatcher), can test the username/credentials in an alternative way. @@ -469,7 +433,7 @@ class Authenticator(Module): self, request: 'types.request.ExtendedHttpRequest', username: str, - ) -> AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ Invoked whenever an user logs out. @@ -495,7 +459,7 @@ class Authenticator(Module): invoked if user requests "log out", but maybe it will never be invoked. """ - return SUCCESS_AUTH + return types.auth.SUCCESS_AUTH def webLogoutHook( self, @@ -566,7 +530,7 @@ class Authenticator(Module): parameters: 'types.auth.AuthCallbackParams', gm: 'GroupsManager', request: 'types.request.ExtendedHttpRequest', - ) -> AuthenticationResult: + ) -> types.auth.AuthenticationResult: """ There is a view inside UDS, an url, that will redirect the petition to this callback. @@ -603,7 +567,7 @@ class Authenticator(Module): There will be calls to getGroups one an again, and also to getRealName, not just at login, but at future (from admin interface, at user editing for example) """ - return FAILED_AUTH + return types.auth.FAILED_AUTH def getInfo( self, parameters: typing.Mapping[str, str] diff --git a/server/src/uds/core/consts/auth.py b/server/src/uds/core/consts/auth.py index 062e68a86..90ea64448 100644 --- a/server/src/uds/core/consts/auth.py +++ b/server/src/uds/core/consts/auth.py @@ -43,4 +43,3 @@ DISABLED: typing.Final[str] = 'd' NO_FILTERING: typing.Final[str] = 'n' ALLOW: typing.Final[str] = 'a' DENY: typing.Final[str] = 'd' - diff --git a/server/src/uds/core/exceptions/__init__.py b/server/src/uds/core/exceptions/__init__.py new file mode 100644 index 000000000..6676f275c --- /dev/null +++ b/server/src/uds/core/exceptions/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2023 Virtual Cable S.L.U. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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.U. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Author: Adolfo Gómez, dkmaster at dkmon dot com +""" +from . import auth +from . import validation +from . import service + +# Common exceptions inserted here +from .common import UDSException, BlockAccess + diff --git a/server/src/uds/core/exceptions/actor.py b/server/src/uds/core/exceptions/actor.py new file mode 100644 index 000000000..1e9f3247b --- /dev/null +++ b/server/src/uds/core/exceptions/actor.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012-2019 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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.U. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Author: Adolfo Gómez, dkmaster at dkmon dot com +""" +from . import common + + +class ActorException(common.UDSException): + """ + Base class for all Actor exceptions + """ + + +# Actor related exceptions +class NoActorComms(ActorException): + """ + Exception used to signal that the actor user service does not have comms url + """ + + pass + + +class OldActorVersion(ActorException): + """ + Exception used to signal that the actor user service version is too old + """ diff --git a/server/src/uds/core/auths/exceptions.py b/server/src/uds/core/exceptions/auth.py similarity index 97% rename from server/src/uds/core/auths/exceptions.py rename to server/src/uds/core/exceptions/auth.py index 4202014c1..75c70b489 100644 --- a/server/src/uds/core/auths/exceptions.py +++ b/server/src/uds/core/exceptions/auth.py @@ -30,9 +30,9 @@ """ Author: Adolfo Gómez, dkmaster at dkmon dot com """ +from . import common - -class AuthenticatorException(Exception): +class AuthenticatorException(common.UDSException): """ Generic authentication exception """ diff --git a/server/src/uds/core/exceptions.py b/server/src/uds/core/exceptions/common.py similarity index 77% rename from server/src/uds/core/exceptions.py rename to server/src/uds/core/exceptions/common.py index 845f309a5..92764a4cf 100644 --- a/server/src/uds/core/exceptions.py +++ b/server/src/uds/core/exceptions/common.py @@ -37,33 +37,8 @@ class UDSException(Exception): Base class for all UDS exceptions """ - -class ValidationError(UDSException): - """ - Exception used to indicate that the params assigned are invalid - """ - - -class TransportError(UDSException): - """ - Exception used to indicate that the transport is not available - """ - - class BlockAccess(UDSException): """ Exception used to signal that the access to a resource is blocked """ -# Actor related exceptions -class NoActorComms(UDSException): - """ - Exception used to signal that the actor user service does not have comms url - """ - pass - - -class OldActorVersion(NoActorComms): - """ - Exception used to signal that the actor user service version is too old - """ diff --git a/server/src/uds/core/exceptions/service.py b/server/src/uds/core/exceptions/service.py new file mode 100644 index 000000000..01f467416 --- /dev/null +++ b/server/src/uds/core/exceptions/service.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2023 Virtual Cable S.L.U. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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.U. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Author: Adolfo Gómez, dkmaster at dkmon dot com +""" + +from .common import UDSException + +class TransportError(UDSException): + """ + Exception used to indicate that the transport is not available + """ + diff --git a/server/src/uds/core/exceptions/validation.py b/server/src/uds/core/exceptions/validation.py new file mode 100644 index 000000000..34d56cc60 --- /dev/null +++ b/server/src/uds/core/exceptions/validation.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2023 Virtual Cable S.L.U. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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.U. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Author: Adolfo Gómez, dkmaster at dkmon dot com +""" + +from .common import UDSException + +class ValidationError(UDSException): + """ + Exception used to indicate that the params assigned are invalid + """ diff --git a/server/src/uds/core/managers/crypto.py b/server/src/uds/core/managers/crypto.py index 47fde88e3..1d0fe03e3 100644 --- a/server/src/uds/core/managers/crypto.py +++ b/server/src/uds/core/managers/crypto.py @@ -52,6 +52,7 @@ from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from django.conf import settings +from regex import E from uds.core.util import singleton @@ -293,14 +294,21 @@ class CryptoManager(metaclass=singleton.Singleton): elif isinstance(obj, bytes): obj = obj.decode('utf8') # To string else: - obj = str(obj) + try: + obj = str(obj) + except Exception: + obj = str(hash(obj)) # Get hash of object return str( uuid.uuid5(self._namespace, obj) ).lower() # I believe uuid returns a lowercase uuid always, but in case... :) - def randomString(self, length: int = 40, digits: bool = True) -> str: - base = string.ascii_letters + (string.digits if digits else '') + def randomString(self, length: int = 40, digits: bool = True, punctuation: bool = False) -> str: + base = ( + string.ascii_letters + + (string.digits if digits else '') + + (string.punctuation if punctuation else '') + ) return ''.join(secrets.choice(base) for _ in range(length)) def unique(self) -> str: diff --git a/server/src/uds/core/managers/downloads.py b/server/src/uds/core/managers/downloads.py index 3006e9eb6..05082af6e 100644 --- a/server/src/uds/core/managers/downloads.py +++ b/server/src/uds/core/managers/downloads.py @@ -63,20 +63,17 @@ class DownloadsManager(metaclass=singleton.Singleton): @staticmethod def manager() -> 'DownloadsManager': - return ( - DownloadsManager() - ) # Singleton pattern will return always the same instance + # Singleton pattern will return always the same instance + return DownloadsManager() - def registerDownloadable( - self, name: str, comment: str, path: str, mime: str = 'application/octet-stream' - ): + def registerDownloadable(self, name: str, comment: str, path: str, mime: str = 'application/octet-stream'): """ Registers a downloadable file. @param name: name shown @param path: path to file @params zip: If download as zip """ - _id = CryptoManager().uuid(name) + _id = CryptoManager.manager().uuid(name) self._downloadables[_id] = { 'name': name, 'comment': comment, @@ -89,9 +86,7 @@ class DownloadsManager(metaclass=singleton.Singleton): def send(self, request, _id) -> HttpResponse: if _id not in self._downloadables: - logger.error( - 'Downloadable id %s not found in %s!!!', _id, self._downloadables - ) + logger.error('Downloadable id %s not found in %s!!!', _id, self._downloadables) raise Http404 return self._send_file( request, @@ -106,8 +101,12 @@ class DownloadsManager(metaclass=singleton.Singleton): memory at once. The FileWrapper will turn the file object into an iterator for chunks of 8KB. """ - wrapper = FileWrapper(open(filename, 'rb')) # pylint: disable=consider-using-with - response = HttpResponse(wrapper, content_type=mime) - response['Content-Length'] = os.path.getsize(filename) - response['Content-Disposition'] = 'attachment; filename=' + name - return response + try: + wrapper = FileWrapper(open(filename, 'rb')) # pylint: disable=consider-using-with + response = HttpResponse(wrapper, content_type=mime) + response['Content-Length'] = os.path.getsize(filename) + response['Content-Disposition'] = 'attachment; filename=' + name + return response + except Exception as e: + logger.error('Error sending file %s: %s', filename, e) + raise Http404 diff --git a/server/src/uds/core/mfas/mfa.py b/server/src/uds/core/mfas/mfa.py index 49051b756..f1946a8d5 100644 --- a/server/src/uds/core/mfas/mfa.py +++ b/server/src/uds/core/mfas/mfa.py @@ -40,8 +40,8 @@ import typing from django.utils.translation import gettext_noop as _, gettext from uds.core.module import Module from uds.core.util.model import getSqlDatetime -from uds.core.auths import exceptions from uds.models.network import Network +from uds.core import exceptions if typing.TYPE_CHECKING: from uds.core.environment import Environment @@ -205,7 +205,7 @@ class MFA(Module): If raises an error, the MFA code was not sent, and the user needs to enter the MFA code. """ logger.error('MFA.sendCode not implemented') - raise exceptions.MFAError('MFA.sendCode not implemented') + raise exceptions.auth.MFAError('MFA.sendCode not implemented') def _getData( self, request: 'ExtendedHttpRequest', userId: str @@ -326,7 +326,7 @@ class MFA(Module): # if it is no more valid, raise an error # Remove stored code and raise error self._removeData(request, userId) - raise exceptions.MFAError('MFA Code expired') + raise exceptions.auth.MFAError('MFA Code expired') # Check if the code is valid if data[1] == code: @@ -337,7 +337,7 @@ class MFA(Module): # Any error means invalid code err = str(e) - raise exceptions.MFAError(err) + raise exceptions.auth.MFAError(err) def resetData( self, @@ -355,7 +355,7 @@ class MFA(Module): """ mfa = user.manager.mfa if not mfa: - raise exceptions.MFAError('MFA is not enabled') + raise exceptions.auth.MFAError('MFA is not enabled') return hashlib.sha3_256( (user.name + (user.uuid or '') + mfa.uuid).encode() diff --git a/server/src/uds/core/types/auth.py b/server/src/uds/core/types/auth.py index 95b0923ac..12241e5a1 100644 --- a/server/src/uds/core/types/auth.py +++ b/server/src/uds/core/types/auth.py @@ -30,12 +30,50 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com """ import typing +import enum + +from django.urls import reverse if typing.TYPE_CHECKING: from django.http import HttpRequest from django.http.request import QueryDict +class AuthenticationState(enum.IntEnum): + """ + Enumeration for authentication success + """ + + FAIL = 0 + SUCCESS = 1 + REDIRECT = 2 + + +class AuthenticationInternalUrl(enum.Enum): + """ + Enumeration for authentication success + """ + + LOGIN = 'page.login' + LOGOUT = 'page.logout' + + def getUrl(self) -> str: + """ + Returns the url for the given internal url + """ + return reverse(self.value) + + +class AuthenticationResult(typing.NamedTuple): + success: AuthenticationState + url: typing.Optional[str] = None + username: typing.Optional[str] = None + +# Comodity values +FAILED_AUTH = AuthenticationResult(success=AuthenticationState.FAIL) +SUCCESS_AUTH = AuthenticationResult(success=AuthenticationState.SUCCESS) + + class AuthCallbackParams(typing.NamedTuple): '''Parameters passed to auth callback stage2 @@ -61,3 +99,4 @@ class AuthCallbackParams(typing.NamedTuple): post_params=request.POST.copy(), query_string=request.META['QUERY_STRING'], ) + diff --git a/server/src/uds/core/ui/user_interface.py b/server/src/uds/core/ui/user_interface.py index fd64d06be..68d2d89aa 100644 --- a/server/src/uds/core/ui/user_interface.py +++ b/server/src/uds/core/ui/user_interface.py @@ -544,12 +544,12 @@ class gui: elif pattern == types.ui.FieldPatternType.HOST: try: validators.validateHostname(self.value, allowDomain=True) - except exceptions.ValidationError: + except exceptions.validation.ValidationError: validators.validateIpv4OrIpv6(self.value) elif pattern == types.ui.FieldPatternType.PATH: validators.validatePath(self.value) return True - except exceptions.ValidationError: + except exceptions.validation.ValidationError: return False elif isinstance(pattern, str): # It's a regex diff --git a/server/src/uds/core/util/auth.py b/server/src/uds/core/util/auth.py index 7212fadb0..86100704a 100644 --- a/server/src/uds/core/util/auth.py +++ b/server/src/uds/core/util/auth.py @@ -55,7 +55,7 @@ def validateRegexField(field: ui.gui.TextField, fieldValue: typing.Optional[str] try: re.search(pattern, '') except Exception as e: - raise exceptions.ValidationError(f'Invalid pattern at {field.label}: {line}') from e + raise exceptions.validation.ValidationError(f'Invalid pattern at {field.label}: {line}') from e def processRegexField( diff --git a/server/src/uds/core/util/validators.py b/server/src/uds/core/util/validators.py index 56766a6d4..34e70e0ac 100644 --- a/server/src/uds/core/util/validators.py +++ b/server/src/uds/core/util/validators.py @@ -69,39 +69,39 @@ def validateNumeric( try: numeric = int(value) if minValue is not None and numeric < minValue: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('{0} must be greater than or equal to {1}').format(fieldName, minValue) ) if maxValue is not None and numeric > maxValue: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('{0} must be lower than or equal to {1}').format(fieldName, maxValue) ) value = str(numeric) except ValueError: - raise exceptions.ValidationError(_('{0} contains invalid characters').format(fieldName)) from None + raise exceptions.validation.ValidationError(_('{0} contains invalid characters').format(fieldName)) from None return int(value) def validateHostname(hostname: str, maxLength: int = 64, allowDomain=False) -> str: if len(hostname) > maxLength: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('{} is not a valid hostname: maximum host name length exceeded.').format(hostname) ) if not allowDomain: if '.' in hostname: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('{} is not a valid hostname: (domains not allowed)').format(hostname) ) allowed = re.compile(r'(?!-)[A-Z\d-]{1,63}(? str: def validateUrl(url: str, maxLength: int = 1024) -> str: if len(url) > maxLength: - raise exceptions.ValidationError(_('{} is not a valid URL: exceeds maximum length.').format(url)) + raise exceptions.validation.ValidationError(_('{} is not a valid URL: exceeds maximum length.').format(url)) try: url_validator(url) except Exception as e: - raise exceptions.ValidationError(str(e)) + raise exceptions.validation.ValidationError(str(e)) return url @@ -132,7 +132,7 @@ def validateIpv4(ipv4: str) -> str: try: dj_validators.validate_ipv4_address(ipv4) except Exception: - raise exceptions.ValidationError(_('{} is not a valid IPv4 address').format(ipv4)) from None + raise exceptions.validation.ValidationError(_('{} is not a valid IPv4 address').format(ipv4)) from None return ipv4 @@ -146,7 +146,7 @@ def validateIpv6(ipv6: str) -> str: try: dj_validators.validate_ipv6_address(ipv6) except Exception: - raise exceptions.ValidationError(_('{} is not a valid IPv6 address').format(ipv6)) from None + raise exceptions.validation.ValidationError(_('{} is not a valid IPv6 address').format(ipv6)) from None return ipv6 @@ -160,7 +160,7 @@ def validateIpv4OrIpv6(ipv4OrIpv6: str) -> str: try: dj_validators.validate_ipv46_address(ipv4OrIpv6) except Exception: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('{} is not a valid IPv4 or IPv6 address').format(ipv4OrIpv6) ) from None return ipv4OrIpv6 @@ -188,20 +188,20 @@ def validatePath( str: path """ if len(path) > maxLength: - raise exceptions.ValidationError(_('{} exceeds maximum path length.').format(path)) + raise exceptions.validation.ValidationError(_('{} exceeds maximum path length.').format(path)) valid_for_windows = re.compile(r'^[a-zA-Z]:\\.*$') valid_for_unix = re.compile(r'^/.*$') if mustBeWindows: if not valid_for_windows.match(path): - raise exceptions.ValidationError(_('{} is not a valid windows path').format(path)) + raise exceptions.validation.ValidationError(_('{} is not a valid windows path').format(path)) elif mustBeUnix: if not valid_for_unix.match(path): - raise exceptions.ValidationError(_('{} is not a valid unix path').format(path)) + raise exceptions.validation.ValidationError(_('{} is not a valid unix path').format(path)) else: if not valid_for_windows.match(path) and not valid_for_unix.match(path): - raise exceptions.ValidationError(_('{} is not a valid path').format(path)) + raise exceptions.validation.ValidationError(_('{} is not a valid path').format(path)) return path @@ -255,7 +255,7 @@ def validateHostPortPair(hostPortPair: str) -> typing.Tuple[str, int]: except Exception: return validateHostname(host, 255, False), validatePort(port) except Exception: - raise exceptions.ValidationError(_('{} is not a valid host:port pair').format(hostPortPair)) from None + raise exceptions.validation.ValidationError(_('{} is not a valid host:port pair').format(hostPortPair)) from None def validateTimeout(timeOutStr: str) -> int: @@ -282,7 +282,7 @@ def validateMac(mac: str) -> str: ) # In fact, it could be XX-XX-XX-XX-XX-XX, but we use - as range separator if macRE.match(mac) is None: - raise exceptions.ValidationError(_('{} is not a valid MAC address').format(mac)) + raise exceptions.validation.ValidationError(_('{} is not a valid MAC address').format(mac)) return mac @@ -298,7 +298,7 @@ def validateMacRange(macRange: str) -> str: validateMac(macRangeStart) validateMac(macRangeEnd) except Exception: - raise exceptions.ValidationError(_('{} is not a valid MAC range').format(macRange)) from None + raise exceptions.validation.ValidationError(_('{} is not a valid MAC range').format(macRange)) from None return macRange @@ -310,10 +310,10 @@ def validateEmail(email: str) -> str: :return: Raises exceptions.Validation exception if is invalid, else return the value "fixed" """ if len(email) > 254: - raise exceptions.ValidationError(_('Email address is too long')) + raise exceptions.validation.ValidationError(_('Email address is too long')) if not re.match(r"[^@]+@[^@]+\.[^@]+", email): - raise exceptions.ValidationError(_('Email address is not valid')) + raise exceptions.validation.ValidationError(_('Email address is not valid')) return email @@ -333,16 +333,16 @@ def validateBasename(baseName: str, length: int = -1) -> str: None -- [description] """ if re.match(r'^[a-zA-Z0-9][a-zA-Z0-9-]*$', baseName) is None: - raise exceptions.ValidationError(_('The basename is not a valid for a hostname')) + raise exceptions.validation.ValidationError(_('The basename is not a valid for a hostname')) if length == 0: - raise exceptions.ValidationError(_('The length of basename plus length must be greater than 0')) + raise exceptions.validation.ValidationError(_('The length of basename plus length must be greater than 0')) if length != -1 and len(baseName) + length > 15: - raise exceptions.ValidationError(_('The length of basename plus length must not be greater than 15')) + raise exceptions.validation.ValidationError(_('The length of basename plus length must not be greater than 15')) if baseName.isdigit(): - raise exceptions.ValidationError(_('The machine name can\'t be only numbers')) + raise exceptions.validation.ValidationError(_('The machine name can\'t be only numbers')) return baseName @@ -355,7 +355,7 @@ def validateJson(jsonData: typing.Optional[str]) -> typing.Any: jsonData (typing.Optional[str]): Json data to validate Raises: - exceptions.ValidationError: If json data is not valid + exceptions.validation.ValidationError: If json data is not valid Returns: typing.Any: Json data as python object @@ -365,7 +365,7 @@ def validateJson(jsonData: typing.Optional[str]) -> typing.Any: try: return json.loads(jsonData) except Exception: - raise exceptions.ValidationError(_('Invalid JSON data')) from None + raise exceptions.validation.ValidationError(_('Invalid JSON data')) from None def validateServerCertificate(cert: typing.Optional[str]) -> str: @@ -376,7 +376,7 @@ def validateServerCertificate(cert: typing.Optional[str]) -> str: cert (str): Certificate to validate Raises: - exceptions.ValidationError: If certificate is not valid + exceptions.validation.ValidationError: If certificate is not valid Returns: str: Certificate @@ -386,7 +386,7 @@ def validateServerCertificate(cert: typing.Optional[str]) -> str: try: security.checkServerCertificateIsValid(cert) except Exception as e: - raise exceptions.ValidationError(_('Invalid certificate') + f' :{e}') from e + raise exceptions.validation.ValidationError(_('Invalid certificate') + f' :{e}') from e return cert @@ -407,6 +407,6 @@ def validateServerCertificateMulti(value: typing.Optional[str]) -> str: try: load_pem_x509_certificate(pemCert.encode()) except Exception as e: - raise exceptions.ValidationError(_('Invalid certificate') + f' :{e}') from e + raise exceptions.validation.ValidationError(_('Invalid certificate') + f' :{e}') from e return value diff --git a/server/src/uds/mfas/Email/mfa.py b/server/src/uds/mfas/Email/mfa.py index 21d39d291..f3aaaeb4b 100644 --- a/server/src/uds/mfas/Email/mfa.py +++ b/server/src/uds/mfas/Email/mfa.py @@ -188,7 +188,7 @@ class EmailMFA(mfas.MFA): # if hostname is not valid, we will raise an exception hostname = self.hostname.cleanStr() if not hostname: - raise exceptions.ValidationError(_('Invalid SMTP hostname')) + raise exceptions.validation.ValidationError(_('Invalid SMTP hostname')) # Now check is valid format if ':' in hostname: diff --git a/server/src/uds/mfas/Radius/mfa.py b/server/src/uds/mfas/Radius/mfa.py index 44fe5cb1f..0318481ed 100644 --- a/server/src/uds/mfas/Radius/mfa.py +++ b/server/src/uds/mfas/Radius/mfa.py @@ -36,7 +36,7 @@ import logging from django.utils.translation import gettext_noop as _, gettext from uds import models -from uds.core import mfas +from uds.core import mfas, exceptions from uds.core.ui import gui from uds.auths.Radius import client @@ -48,7 +48,6 @@ from uds.auths.Radius.client import ( # NEEDED ) from uds.core.auths.auth import webPassword -from uds.core.auths import exceptions if typing.TYPE_CHECKING: from uds.core.module import Module @@ -291,4 +290,4 @@ class RadiusOTP(mfas.MFA): username, request.ip, ) - raise exceptions.MFAError(err) + raise exceptions.auth.MFAError(err) diff --git a/server/src/uds/mfas/TOTP/mfa.py b/server/src/uds/mfas/TOTP/mfa.py index e43a7f42f..130761273 100644 --- a/server/src/uds/mfas/TOTP/mfa.py +++ b/server/src/uds/mfas/TOTP/mfa.py @@ -40,11 +40,9 @@ from django.utils.translation import gettext_noop as _, gettext from uds import models from uds.core.util.model import getSqlDatetime -from uds.core import mfas +from uds.core import mfas, exceptions from uds.core.ui import gui -from uds.core.auths import exceptions - if typing.TYPE_CHECKING: from uds.core.module import Module from uds.core.types.request import ExtendedHttpRequest @@ -201,7 +199,7 @@ class TOTP_MFA(mfas.MFA): return if self.cache.get(userId + code) is not None: - raise exceptions.MFAError(gettext('Code is already used. Wait a minute and try again.')) + raise exceptions.auth.MFAError(gettext('Code is already used. Wait a minute and try again.')) # Get data from storage related to this user secret, qrShown = self._userData(userId) @@ -210,7 +208,7 @@ class TOTP_MFA(mfas.MFA): if not self.getTOTP(userId, username).verify( code, valid_window=self.validWindow.num(), for_time=getSqlDatetime() ): - raise exceptions.MFAError(gettext('Invalid code')) + raise exceptions.auth.MFAError(gettext('Invalid code')) self.cache.put(userId + code, True, self.validWindow.num() * (TOTP_INTERVAL + 1)) diff --git a/server/src/uds/models/user.py b/server/src/uds/models/user.py index 6eecc5102..0137a82d5 100644 --- a/server/src/uds/models/user.py +++ b/server/src/uds/models/user.py @@ -35,7 +35,7 @@ import typing from django.db import models from django.db.models import Count, Q, signals -from uds.core import auths, mfas +from uds.core import auths, mfas, types from uds.core.util import log, storage from .authenticator import Authenticator @@ -136,7 +136,7 @@ class User(UUIDModel): self.last_access = getSqlDatetime() self.save(update_fields=['last_access']) - def logout(self, request: 'ExtendedHttpRequest') -> auths.AuthenticationResult: + def logout(self, request: 'ExtendedHttpRequest') -> types.auth.AuthenticationResult: """ Invoked to log out this user Returns the url where to redirect user, or None if default url will be used diff --git a/server/src/uds/notifiers/email/notifier.py b/server/src/uds/notifiers/email/notifier.py index 5aa09d7ed..bfcf77dfa 100644 --- a/server/src/uds/notifiers/email/notifier.py +++ b/server/src/uds/notifiers/email/notifier.py @@ -148,7 +148,7 @@ class EmailNotifier(messaging.Notifier): # if hostname is not valid, we will raise an exception hostname = self.hostname.cleanStr() if not hostname: - raise exceptions.ValidationError(_('Invalid SMTP hostname')) + raise exceptions.validation.ValidationError(_('Invalid SMTP hostname')) # Now check is valid format if ':' in hostname: diff --git a/server/src/uds/notifiers/telegram/notifier.py b/server/src/uds/notifiers/telegram/notifier.py index 86fbbd19c..4977d77a8 100644 --- a/server/src/uds/notifiers/telegram/notifier.py +++ b/server/src/uds/notifiers/telegram/notifier.py @@ -128,7 +128,7 @@ class TelegramNotifier(messaging.Notifier): for i in (self.botname, self.accessToken, self.secret): s = i.cleanStr() if not s: - raise exceptions.ValidationError(_('Invalid value for {}').format(i.label)) + raise exceptions.validation.ValidationError(_('Invalid value for {}').format(i.label)) i.value = s def initGui(self) -> None: diff --git a/server/src/uds/osmanagers/LinuxOsManager/linux_ad_osmanager.py b/server/src/uds/osmanagers/LinuxOsManager/linux_ad_osmanager.py index 6ef4c82b8..de2538835 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/linux_ad_osmanager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/linux_ad_osmanager.py @@ -155,11 +155,11 @@ class LinuxOsADManager(LinuxOsManager): super().__init__(environment, values) if values: if values['domain'] == '': - raise exceptions.ValidationError(_('Must provide a domain!')) + raise exceptions.validation.ValidationError(_('Must provide a domain!')) if values['account'] == '': - raise exceptions.ValidationError(_('Must provide an account to add machines to domain!')) + raise exceptions.validation.ValidationError(_('Must provide an account to add machines to domain!')) if values['password'] == '': - raise exceptions.ValidationError(_('Must provide a password for the account!')) + raise exceptions.validation.ValidationError(_('Must provide a password for the account!')) self._domain = values['domain'] self._account = values['account'] self._password = values['password'] diff --git a/server/src/uds/osmanagers/LinuxOsManager/linux_freeipa_osmanager.py b/server/src/uds/osmanagers/LinuxOsManager/linux_freeipa_osmanager.py index 6b2f8be8c..ef88a49ba 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/linux_freeipa_osmanager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/linux_freeipa_osmanager.py @@ -146,11 +146,11 @@ class LinuxOsFreeIPAManager(LinuxOsManager): super().__init__(environment, values) if values: if values['domain'] == '': - raise exceptions.ValidationError(_('Must provide a domain!')) + raise exceptions.validation.ValidationError(_('Must provide a domain!')) if values['account'] == '': - raise exceptions.ValidationError(_('Must provide an account to add machines to domain!')) + raise exceptions.validation.ValidationError(_('Must provide an account to add machines to domain!')) if values['password'] == '': - raise exceptions.ValidationError(_('Must provide a password for the account!')) + raise exceptions.validation.ValidationError(_('Must provide a password for the account!')) self._domain = values['domain'] self._account = values['account'] self._password = values['password'] diff --git a/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py b/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py index dd930314a..6c75ccd9d 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py @@ -77,7 +77,7 @@ class LinuxRandomPassManager(LinuxOsManager): super().__init__(environment, values) if values is not None: if values['userAccount'] == '': - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Must provide an user account!!!') ) self._userAccount = values['userAccount'] diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows.py b/server/src/uds/osmanagers/WindowsOsManager/windows.py index 4df7021ad..95f99f64f 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows.py @@ -96,11 +96,11 @@ class WindowsOsManager(osmanagers.OSManager): try: length = int(length) except Exception: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Length must be numeric!!') ) from None if length > 6 or length < 1: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Length must be betwen 1 and 6') ) return length diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py b/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py index d2f0ff314..985de1d90 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py @@ -142,15 +142,15 @@ class WinDomainOsManager(WindowsOsManager): super().__init__(environment, values) if values: if values['domain'] == '': - raise exceptions.ValidationError(_('Must provide a domain!')) + raise exceptions.validation.ValidationError(_('Must provide a domain!')) # if values['domain'].find('.') == -1: # raise exceptions.ValidationException(_('Must provide domain in FQDN')) if values['account'] == '': - raise exceptions.ValidationError(_('Must provide an account to add machines to domain!')) + raise exceptions.validation.ValidationError(_('Must provide an account to add machines to domain!')) if values['account'].find('\\') != -1: - raise exceptions.ValidationError(_('DOM\\USER form is not allowed!')) + raise exceptions.validation.ValidationError(_('DOM\\USER form is not allowed!')) if values['password'] == '': - raise exceptions.ValidationError(_('Must provide a password for the account!')) + raise exceptions.validation.ValidationError(_('Must provide a password for the account!')) self._domain = values['domain'] self._ou = values['ou'].strip() self._account = values['account'] diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows_random.py b/server/src/uds/osmanagers/WindowsOsManager/windows_random.py index ce696e235..223567ec3 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows_random.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows_random.py @@ -86,9 +86,9 @@ class WinRandomPassManager(WindowsOsManager): super().__init__(environment, values) if values: if values['userAccount'] == '': - raise exceptions.ValidationError(_('Must provide an user account!!!')) + raise exceptions.validation.ValidationError(_('Must provide an user account!!!')) if values['password'] == '': - raise exceptions.ValidationError(_('Must provide a password for the account!!!')) + raise exceptions.validation.ValidationError(_('Must provide a password for the account!!!')) self._userAccount = values['userAccount'] self._password = values['password'] else: diff --git a/server/src/uds/services/OVirt/service.py b/server/src/uds/services/OVirt/service.py index 25037680d..20b491cbb 100644 --- a/server/src/uds/services/OVirt/service.py +++ b/server/src/uds/services/OVirt/service.py @@ -225,7 +225,7 @@ class OVirtLinkedService(services.Service): # pylint: disable=too-many-public-m if values: validators.validateBasename(self.baseName.value, self.lenName.num()) if int(self.memory.value) < 256 or int(self.memoryGuaranteed.value) < 256: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('The minimum allowed memory is 256 Mb') ) if int(self.memoryGuaranteed.value) > int(self.memory.value): diff --git a/server/src/uds/services/PhysicalMachines/provider.py b/server/src/uds/services/PhysicalMachines/provider.py index 8acc814f2..ff1426ccb 100644 --- a/server/src/uds/services/PhysicalMachines/provider.py +++ b/server/src/uds/services/PhysicalMachines/provider.py @@ -87,13 +87,13 @@ class PhysicalMachinesProvider(services.ServiceProvider): config.read_string(self.config.value) # Seems a valid configuration file, let's see if all se except Exception as e: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Invalid advanced configuration: ') + str(e) ) for section in config.sections(): if section not in VALID_CONFIG_SECTIONS: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Invalid section in advanced configuration: ') + section ) @@ -103,12 +103,12 @@ class PhysicalMachinesProvider(services.ServiceProvider): try: net.networksFromString(key) # Raises exception if net is invalid except Exception: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Invalid network in advanced configuration: ') + key ) from None # Now check value is an url if config['wol'][key][:4] != 'http': - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Invalid url in advanced configuration: ') + key ) diff --git a/server/src/uds/services/PhysicalMachines/service_multi.py b/server/src/uds/services/PhysicalMachines/service_multi.py index ea2431b1c..586f47111 100644 --- a/server/src/uds/services/PhysicalMachines/service_multi.py +++ b/server/src/uds/services/PhysicalMachines/service_multi.py @@ -154,7 +154,7 @@ class IPMachinesService(IPServiceBase): # Check that ips are valid for v in values['ipList']: if not net.isValidHost(v.split(';')[0]): # Get only IP/hostname - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('Invalid value detected on servers list: "{}"').format(v) ) self._ips = [ diff --git a/server/src/uds/services/PhysicalMachines/service_single.py b/server/src/uds/services/PhysicalMachines/service_single.py index 3c46fea9a..ad03c882e 100644 --- a/server/src/uds/services/PhysicalMachines/service_single.py +++ b/server/src/uds/services/PhysicalMachines/service_single.py @@ -80,7 +80,7 @@ class IPSingleMachineService(IPServiceBase): return if not net.isValidHost(self.ip.value): - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( gettext('Invalid server used: "{}"'.format(self.ip.value)) ) diff --git a/server/src/uds/services/Sample/provider.py b/server/src/uds/services/Sample/provider.py index d0aa2f1ad..fc4b120db 100644 --- a/server/src/uds/services/Sample/provider.py +++ b/server/src/uds/services/Sample/provider.py @@ -181,7 +181,7 @@ class Provider(services.ServiceProvider): # values are only passed from administration client. Internals # instantiations are always empty. if values and self.methAlive.isTrue(): - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('Methuselah is not alive!!! :-)') ) @@ -222,7 +222,7 @@ class Provider(services.ServiceProvider): instance.methAge.value, instance.methAlive.value, ) - except exceptions.ValidationError as e: + except exceptions.validation.ValidationError as e: # If we say that meth is alive, instantiation will return [False, str(e)] except Exception as e: diff --git a/server/src/uds/services/Sample/service.py b/server/src/uds/services/Sample/service.py index b129fbf2d..571823712 100644 --- a/server/src/uds/services/Sample/service.py +++ b/server/src/uds/services/Sample/service.py @@ -164,7 +164,7 @@ class ServiceOne(services.Service): # so we only need to validate params if values is not None if values: if self.colour.value == 'nonsense': - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( 'The selected colour is invalid!!!' ) diff --git a/server/src/uds/services/Xen/service.py b/server/src/uds/services/Xen/service.py index e4fd93330..9e60b6e11 100644 --- a/server/src/uds/services/Xen/service.py +++ b/server/src/uds/services/Xen/service.py @@ -190,7 +190,7 @@ class XenLinkedService(services.Service): # pylint: disable=too-many-public-met validators.validateBasename(self.baseName.value, self.lenName.num()) if int(self.memory.value) < 256: - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('The minimum allowed memory is 256 Mb') ) diff --git a/server/src/uds/transports/SPICE/spice.py b/server/src/uds/transports/SPICE/spice.py index fa15a26fa..2011ec2e2 100644 --- a/server/src/uds/transports/SPICE/spice.py +++ b/server/src/uds/transports/SPICE/spice.py @@ -90,7 +90,7 @@ class SPICETransport(BaseSpiceTransport): logger.debug('Connection data: %s', con) if not con: - raise exceptions.TransportError('No console connection data') + raise exceptions.service.TransportError('No console connection data') port: str = con['port'] or '-1' secure_port: str = con['secure_port'] or '-1' diff --git a/server/src/uds/transports/SPICE/spicetunnel.py b/server/src/uds/transports/SPICE/spicetunnel.py index df49b85dd..33dee8d15 100644 --- a/server/src/uds/transports/SPICE/spicetunnel.py +++ b/server/src/uds/transports/SPICE/spicetunnel.py @@ -106,7 +106,7 @@ class TSPICETransport(BaseSpiceTransport): raise if not con: - raise exceptions.TransportError( + raise exceptions.service.TransportError( _('No console connection data received'), ) diff --git a/server/src/uds/transports/Test/transport.py b/server/src/uds/transports/Test/transport.py index f55103ee0..4694e1170 100644 --- a/server/src/uds/transports/Test/transport.py +++ b/server/src/uds/transports/Test/transport.py @@ -89,7 +89,7 @@ class TestTransport(transports.Transport): self.testURL.value.startswith('http://') or self.testURL.value.startswith('https://') ): - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('The url must be http or https') ) diff --git a/server/src/uds/transports/URL/url_custom.py b/server/src/uds/transports/URL/url_custom.py index 3ed8913ba..978c3e948 100644 --- a/server/src/uds/transports/URL/url_custom.py +++ b/server/src/uds/transports/URL/url_custom.py @@ -90,7 +90,7 @@ class URLCustomTransport(transports.Transport): self.urlPattern.value.startswith('http://') or self.urlPattern.value.startswith('https://') ): - raise exceptions.ValidationError( + raise exceptions.validation.ValidationError( _('The url must be http or https') ) diff --git a/server/src/uds/urls.py b/server/src/uds/urls.py index 03c1701f3..700b59570 100644 --- a/server/src/uds/urls.py +++ b/server/src/uds/urls.py @@ -36,7 +36,7 @@ from django.views.i18n import JavaScriptCatalog from django.views.generic.base import RedirectView from uds import REST -from uds.core.auths.authenticator import AuthenticationInternalUrl +from uds.core import types import uds.web.views import uds.admin.views @@ -85,7 +85,7 @@ urlpatterns = [ # Index path(r'uds/page/services', uds.web.views.main.index, name='page.index'), # Login/logout - path(r'uds/page/login', uds.web.views.main.login, name=AuthenticationInternalUrl.LOGIN.value), + path(r'uds/page/login', uds.web.views.main.login, name=types.auth.AuthenticationInternalUrl.LOGIN.value), re_path( r'^uds/page/login/(?P[a-zA-Z0-9-]+)$', uds.web.views.main.login, diff --git a/server/src/uds/web/views/main.py b/server/src/uds/web/views/main.py index 819fd69d7..150e11415 100644 --- a/server/src/uds/web/views/main.py +++ b/server/src/uds/web/views/main.py @@ -45,7 +45,7 @@ from django.utils.translation import gettext as _ from uds.core.types.request import ExtendedHttpRequest from uds.core.types.request import ExtendedHttpRequestWithUser -from uds.core.auths import auth, exceptions, AuthenticationState +from uds.core.auths import auth from uds.core.util.config import GlobalConfig from uds.core.managers.crypto import CryptoManager from uds.core.managers.user_service import UserServiceManager @@ -55,7 +55,7 @@ from uds.web.forms.MFAForm import MFAForm from uds.web.util.authentication import checkLogin from uds.web.util.services import getServicesData from uds.web.util import configjs -from uds.core import mfas, types +from uds.core import mfas, types, exceptions from uds import auths, models from uds.core.util.model import getSqlStampInSeconds @@ -151,7 +151,7 @@ def logout(request: ExtendedHttpRequestWithUser) -> HttpResponse: request.session['restricted'] = False # Remove restricted request.authorized = False logoutResponse = request.user.logout(request) - url = logoutResponse.url if logoutResponse.success == AuthenticationState.REDIRECT else None + url = logoutResponse.url if logoutResponse.success == types.auth.AuthenticationState.REDIRECT else None return auth.webLogout(request, url or request.session.get('logouturl', None)) @@ -256,7 +256,7 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse: # pylint: disable=too-ma ) return response - except exceptions.MFAError as e: + except exceptions.auth.MFAError as e: logger.error('MFA error: %s', e) tries += 1 request.session['mfa_tries'] = tries