mirror of
https://github.com/dkmstr/openuds.git
synced 2024-12-25 23:21:41 +03:00
Merge remote-tracking branch 'origin/v3.6'
This commit is contained in:
commit
33258b0dcc
@ -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
|
||||||
"""
|
"""
|
||||||
|
@ -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'
|
||||||
|
@ -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")',
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user