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'
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:
"""
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
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
"""

View File

@ -6,8 +6,9 @@ import ssl
import typing
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.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,35 @@ class EmailMFA(mfas.MFA):
# now check from email and to email
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:
return 'OTP received via email'

View File

@ -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(
@ -295,6 +318,9 @@ class SMSMFA(mfas.MFA):
def label(self) -> str:
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:
logger.debug(
'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()
if not mfaIdentifier:
if mfaInstance.emptyIndentifierAllowedToLogin():
if mfaInstance.emptyIndentifierAllowedToLogin(request):
# Allow login
request.authorized = True
return HttpResponseRedirect(reverse('page.index'))
@ -284,10 +284,14 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
else:
remember_device = _('{} hours').format(mfaProvider.remember_device)
# Html from MFA provider
mfaHtml = mfaInstance.html(request)
# Redirect to index, but with MFA data
request.session['mfa'] = {
'label': label or _('MFA Code'),
'validity': validity if validity >= 0 else 0,
'remember_device': remember_device,
'html': mfaHtml,
}
return index(request) # Render index with MFA data