diff --git a/server/src/uds/core/mfas/mfa.py b/server/src/uds/core/mfas/mfa.py index 88e848bc..7da5bfaa 100644 --- a/server/src/uds/core/mfas/mfa.py +++ b/server/src/uds/core/mfas/mfa.py @@ -127,7 +127,7 @@ class MFA(Module): """ return self.cacheTime - def emptyIndentifierAllowedToLogin(self) -> bool: + def emptyIndentifierAllowedToLogin(self, request: 'ExtendedHttpRequest') -> bool: """ If this method returns True, an user that has no "identifier" is allowed to login without MFA """ diff --git a/server/src/uds/mfas/Email/mfa.py b/server/src/uds/mfas/Email/mfa.py index 4ab15833..d64fc08e 100644 --- a/server/src/uds/mfas/Email/mfa.py +++ b/server/src/uds/mfas/Email/mfa.py @@ -8,6 +8,7 @@ import logging from django.utils.translation import gettext_noop as _ +from uds import models from uds.core import mfas from uds.core.ui import gui from uds.core.util import validators, decorators @@ -96,6 +97,32 @@ class EmailMFA(mfas.MFA): tab=_('Config'), ) + + allowLoginWithoutMFA = gui.ChoiceField( + label=_('User without MFA policy'), + order=31, + defaultValue='0', + tooltip=_('Action for SMS response error'), + required=True, + values={ + '0': _('Allow user login'), + '1': _('Deny user login'), + '2': _('Allow user to login if it IP is in the networks list'), + '3': _('Deny user to login if it IP is in the networks list'), + }, + tab=_('Config'), + ) + + networks = gui.MultiChoiceField( + label=_('SMS networks'), + rdonly=False, + rows=5, + order=32, + tooltip=_('Networks for SMS authentication'), + required=True, + tab=_('Config'), + ) + def initialize(self, values: 'Module.ValuesType' = None): """ We will use the "autosave" feature for form fields @@ -123,7 +150,32 @@ class EmailMFA(mfas.MFA): # now check from email and to email self.fromEmail.value = validators.validateEmail(self.fromEmail.value) - # Done + @classmethod + def initClassGui(cls) -> None: + # Populate the networks list + cls.networks.setValues([ + gui.choiceItem(v.uuid, v.name) + for v in models.Network.objects.all().order_by('name') + ]) + + + def checkAction(self, action: str, request: 'ExtendedHttpRequest') -> bool: + def checkIp() -> bool: + return any(i.ipInNetwork(request.ip) for i in models.Network.objects.filter(uuid__in = self.networks.value)) + + if action == '0': + return True + elif action == '1': + return False + elif action == '2': + return checkIp() + elif action == '3': + return not checkIp() + else: + return False + + def emptyIndentifierAllowedToLogin(self, request: 'ExtendedHttpRequest') -> bool: + return self.checkAction(self.allowLoginWithoutMFA.value, request) def label(self) -> str: return 'OTP received via email' diff --git a/server/src/uds/mfas/SMS/mfa.py b/server/src/uds/mfas/SMS/mfa.py index 56d9381b..5e624851 100644 --- a/server/src/uds/mfas/SMS/mfa.py +++ b/server/src/uds/mfas/SMS/mfa.py @@ -43,12 +43,12 @@ class SMSMFA(mfas.MFA): ignoreCertificateErrors = gui.CheckBoxField( label=_('Ignore certificate errors'), order=2, - tab=_('HTTP Server'), defvalue=False, tooltip=_( 'If checked, the server certificate will be ignored. This is ' 'useful if the server uses a self-signed certificate.' ), + tab=_('HTTP Server'), ) sendingMethod = gui.ChoiceField( @@ -56,8 +56,8 @@ class SMSMFA(mfas.MFA): order=3, tooltip=_('Method for sending SMS'), required=True, - tab=_('HTTP Server'), values=('GET', 'POST', 'PUT'), + tab=_('HTTP Server'), ) headersParameters = gui.TextField( @@ -101,8 +101,8 @@ class SMSMFA(mfas.MFA): order=5, tooltip=_('Encoding for SMS'), required=True, - tab=_('HTTP Server'), values=('utf-8', 'iso-8859-1'), + tab=_('HTTP Server'), ) authenticationMethod = gui.ChoiceField( @@ -110,12 +110,12 @@ class SMSMFA(mfas.MFA): order=20, tooltip=_('Method for sending SMS'), required=True, - tab=_('HTTP Authentication'), values={ '0': _('None'), '1': _('HTTP Basic Auth'), '2': _('HTTP Digest Auth'), }, + tab=_('HTTP Authentication'), ) authenticationUserOrToken = gui.TextField( @@ -153,13 +153,28 @@ class SMSMFA(mfas.MFA): defaultValue='0', tooltip=_('Action for SMS response error'), required=True, - tab=_('HTTP Response'), values={ - '0': _('Allow user log in without MFA'), - '1': _('Deny user log in'), - '2': _('Allow user to log in if it IP is in the networks list'), - '3': _('Deny user to log in if it IP is in the networks list'), + '0': _('Allow user login'), + '1': _('Deny user login'), + '2': _('Allow user to login if it IP is in the networks list'), + '3': _('Deny user to login if it IP is in the networks list'), }, + tab=_('Config'), + ) + + allowLoginWithoutMFA = gui.ChoiceField( + label=_('User without MFA policy'), + order=33, + defaultValue='0', + tooltip=_('Action for SMS response error'), + required=True, + values={ + '0': _('Allow user login'), + '1': _('Deny user login'), + '2': _('Allow user to login if it IP is in the networks list'), + '3': _('Deny user to login if it IP is in the networks list'), + }, + tab=_('Config'), ) networks = gui.MultiChoiceField( @@ -169,7 +184,7 @@ class SMSMFA(mfas.MFA): order=32, tooltip=_('Networks for SMS authentication'), required=True, - tab=_('HTTP Response'), + tab=_('Config'), ) def initialize(self, values: 'Module.ValuesType') -> None: @@ -215,6 +230,25 @@ class SMSMFA(mfas.MFA): session.headers[headerName.strip()] = headerValue.strip() return session + + def checkAction(self, action: str, request: 'ExtendedHttpRequest') -> bool: + def checkIp() -> bool: + return any(i.ipInNetwork(request.ip) for i in models.Network.objects.filter(uuid__in = self.networks.value)) + + if action == '0': + return True + elif action == '1': + return False + elif action == '2': + return checkIp() + elif action == '3': + return not checkIp() + else: + return False + + def emptyIndentifierAllowedToLogin(self, request: 'ExtendedHttpRequest') -> bool: + return self.checkAction(self.allowLoginWithoutMFA.value, request) + def processResponse(self, request: 'ExtendedHttpRequest', response: requests.Response) -> mfas.MFA.RESULT: logger.debug('Response: %s', response) if not response.ok: @@ -227,20 +261,9 @@ class SMSMFA(mfas.MFA): 'SMS response error: %s', response.text, ) - if self.responseErrorAction.value == '0': - return mfas.MFA.RESULT.ALLOWED - elif self.responseErrorAction.value == '1': - raise Exception('SMS response error') - else: - isInNetwork = any(i.ipInNetwork(request.ip) for i in models.Network.objects.filter(uuid__in = self.networks.value)) - if self.responseErrorAction.value == '2': - # Allow user to log in if it IP is in the networks list - if isInNetwork: - return mfas.MFA.RESULT.ALLOWED - elif self.responseErrorAction.value == '3': - if isInNetwork: - raise Exception('SMS response error') - return mfas.MFA.RESULT.ALLOWED + if not self.checkAction(self.responseErrorAction.value, request): + raise Exception(_('SMS response error')) + return mfas.MFA.RESULT.ALLOWED return mfas.MFA.RESULT.OK def getData( diff --git a/server/src/uds/web/views/modern.py b/server/src/uds/web/views/modern.py index 79eae89e..37ca15d3 100644 --- a/server/src/uds/web/views/modern.py +++ b/server/src/uds/web/views/modern.py @@ -205,7 +205,7 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse: label = mfaInstance.label() if not mfaIdentifier: - if mfaInstance.emptyIndentifierAllowedToLogin(): + if mfaInstance.emptyIndentifierAllowedToLogin(request): # Allow login request.authorized = True return HttpResponseRedirect(reverse('page.index'))