Merge remote-tracking branch 'origin/v3.6'

This commit is contained in:
Adolfo Gómez García 2022-07-10 12:58:44 +02:00
commit 33258b0dcc
4 changed files with 120 additions and 28 deletions

View File

@ -119,6 +119,13 @@ class MFA(Module):
""" """
return 'MFA Code' return 'MFA Code'
def html(self, request: 'ExtendedHttpRequest') -> str:
"""
This method will be invoked from the MFA form, to know the HTML that will be presented
to the user below the MFA code form.
"""
return ''
def validity(self) -> int: def validity(self) -> int:
""" """
This method will be invoked from the MFA form, to know the validity in secods This method will be invoked from the MFA form, to know the validity in secods
@ -127,7 +134,7 @@ class MFA(Module):
""" """
return self.cacheTime 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 If this method returns True, an user that has no "identifier" is allowed to login without MFA
""" """

View File

@ -6,8 +6,9 @@ import ssl
import typing import typing
import logging import logging
from django.utils.translation import gettext_noop as _ from django.utils.translation import gettext_noop as _, gettext
from uds import models
from uds.core import mfas from uds.core import mfas
from uds.core.ui import gui from uds.core.ui import gui
from uds.core.util import validators, decorators from uds.core.util import validators, decorators
@ -96,6 +97,32 @@ class EmailMFA(mfas.MFA):
tab=_('Config'), 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): def initialize(self, values: 'Module.ValuesType' = None):
""" """
We will use the "autosave" feature for form fields We will use the "autosave" feature for form fields
@ -123,7 +150,35 @@ class EmailMFA(mfas.MFA):
# now check from email and to email # now check from email and to email
self.fromEmail.value = validators.validateEmail(self.fromEmail.value) self.fromEmail.value = validators.validateEmail(self.fromEmail.value)
# Done def html(self, request: 'ExtendedHttpRequest') -> str:
return gettext('Check your mail. You will receive an email with the verification code')
@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: def label(self) -> str:
return 'OTP received via email' return 'OTP received via email'

View File

@ -43,12 +43,12 @@ class SMSMFA(mfas.MFA):
ignoreCertificateErrors = gui.CheckBoxField( ignoreCertificateErrors = gui.CheckBoxField(
label=_('Ignore certificate errors'), label=_('Ignore certificate errors'),
order=2, order=2,
tab=_('HTTP Server'),
defvalue=False, defvalue=False,
tooltip=_( tooltip=_(
'If checked, the server certificate will be ignored. This is ' 'If checked, the server certificate will be ignored. This is '
'useful if the server uses a self-signed certificate.' 'useful if the server uses a self-signed certificate.'
), ),
tab=_('HTTP Server'),
) )
sendingMethod = gui.ChoiceField( sendingMethod = gui.ChoiceField(
@ -56,8 +56,8 @@ class SMSMFA(mfas.MFA):
order=3, order=3,
tooltip=_('Method for sending SMS'), tooltip=_('Method for sending SMS'),
required=True, required=True,
tab=_('HTTP Server'),
values=('GET', 'POST', 'PUT'), values=('GET', 'POST', 'PUT'),
tab=_('HTTP Server'),
) )
headersParameters = gui.TextField( headersParameters = gui.TextField(
@ -101,8 +101,8 @@ class SMSMFA(mfas.MFA):
order=5, order=5,
tooltip=_('Encoding for SMS'), tooltip=_('Encoding for SMS'),
required=True, required=True,
tab=_('HTTP Server'),
values=('utf-8', 'iso-8859-1'), values=('utf-8', 'iso-8859-1'),
tab=_('HTTP Server'),
) )
authenticationMethod = gui.ChoiceField( authenticationMethod = gui.ChoiceField(
@ -110,12 +110,12 @@ class SMSMFA(mfas.MFA):
order=20, order=20,
tooltip=_('Method for sending SMS'), tooltip=_('Method for sending SMS'),
required=True, required=True,
tab=_('HTTP Authentication'),
values={ values={
'0': _('None'), '0': _('None'),
'1': _('HTTP Basic Auth'), '1': _('HTTP Basic Auth'),
'2': _('HTTP Digest Auth'), '2': _('HTTP Digest Auth'),
}, },
tab=_('HTTP Authentication'),
) )
authenticationUserOrToken = gui.TextField( authenticationUserOrToken = gui.TextField(
@ -153,13 +153,28 @@ class SMSMFA(mfas.MFA):
defaultValue='0', defaultValue='0',
tooltip=_('Action for SMS response error'), tooltip=_('Action for SMS response error'),
required=True, required=True,
tab=_('HTTP Response'),
values={ values={
'0': _('Allow user log in without MFA'), '0': _('Allow user login'),
'1': _('Deny user log in'), '1': _('Deny user login'),
'2': _('Allow user to log in if it IP is in the networks list'), '2': _('Allow user to login if it IP is in the networks list'),
'3': _('Deny user to log in 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( networks = gui.MultiChoiceField(
@ -169,7 +184,7 @@ class SMSMFA(mfas.MFA):
order=32, order=32,
tooltip=_('Networks for SMS authentication'), tooltip=_('Networks for SMS authentication'),
required=True, required=True,
tab=_('HTTP Response'), tab=_('Config'),
) )
def initialize(self, values: 'Module.ValuesType') -> None: def initialize(self, values: 'Module.ValuesType') -> None:
@ -215,6 +230,25 @@ class SMSMFA(mfas.MFA):
session.headers[headerName.strip()] = headerValue.strip() session.headers[headerName.strip()] = headerValue.strip()
return session 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: def processResponse(self, request: 'ExtendedHttpRequest', response: requests.Response) -> mfas.MFA.RESULT:
logger.debug('Response: %s', response) logger.debug('Response: %s', response)
if not response.ok: if not response.ok:
@ -227,20 +261,9 @@ class SMSMFA(mfas.MFA):
'SMS response error: %s', 'SMS response error: %s',
response.text, response.text,
) )
if self.responseErrorAction.value == '0': if not self.checkAction(self.responseErrorAction.value, request):
return mfas.MFA.RESULT.ALLOWED raise Exception(_('SMS response error'))
elif self.responseErrorAction.value == '1': return mfas.MFA.RESULT.ALLOWED
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
return mfas.MFA.RESULT.OK return mfas.MFA.RESULT.OK
def getData( def getData(
@ -295,6 +318,9 @@ class SMSMFA(mfas.MFA):
def label(self) -> str: def label(self) -> str:
return gettext('MFA Code') return gettext('MFA Code')
def html(self, request: 'ExtendedHttpRequest') -> str:
return gettext('Check your phone. You will receive an SMS with the verification code')
def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT: def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> mfas.MFA.RESULT:
logger.debug( logger.debug(
'Sending SMS code "%s" for user %s (userId="%s", identifier="%s")', 'Sending SMS code "%s" for user %s (userId="%s", identifier="%s")',

View File

@ -203,7 +203,7 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
label = mfaInstance.label() label = mfaInstance.label()
if not mfaIdentifier: if not mfaIdentifier:
if mfaInstance.emptyIndentifierAllowedToLogin(): if mfaInstance.emptyIndentifierAllowedToLogin(request):
# Allow login # Allow login
request.authorized = True request.authorized = True
return HttpResponseRedirect(reverse('page.index')) return HttpResponseRedirect(reverse('page.index'))
@ -284,10 +284,14 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
else: else:
remember_device = _('{} hours').format(mfaProvider.remember_device) remember_device = _('{} hours').format(mfaProvider.remember_device)
# Html from MFA provider
mfaHtml = mfaInstance.html(request)
# Redirect to index, but with MFA data # Redirect to index, but with MFA data
request.session['mfa'] = { request.session['mfa'] = {
'label': label or _('MFA Code'), 'label': label or _('MFA Code'),
'validity': validity if validity >= 0 else 0, 'validity': validity if validity >= 0 else 0,
'remember_device': remember_device, 'remember_device': remember_device,
'html': mfaHtml,
} }
return index(request) # Render index with MFA data return index(request) # Render index with MFA data