forked from shaba/openuds
Merge remote-tracking branch 'origin/v3.6'
This commit is contained in:
commit
78c7039c54
@ -1,16 +1,12 @@
|
||||
import io
|
||||
import logging
|
||||
import enum
|
||||
import typing
|
||||
|
||||
from pyrad.client import Client
|
||||
from pyrad.dictionary import Dictionary
|
||||
import pyrad.packet
|
||||
|
||||
__all__ = ['RadiusClient', 'RadiusAuthenticationError', 'RADDICT']
|
||||
|
||||
class RadiusAuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RADDICT = """ATTRIBUTE User-Name 1 string
|
||||
@ -51,6 +47,36 @@ ATTRIBUTE Framed-AppleTalk-Link 37 integer
|
||||
ATTRIBUTE Framed-AppleTalk-Network 38 integer
|
||||
ATTRIBUTE Framed-AppleTalk-Zone 39 string"""
|
||||
|
||||
# For AccessChallenge return values
|
||||
NOT_CHECKED, INCORRECT, CORRECT = -1, 0, 1 # for pwd and otp
|
||||
NOT_NEEDED, NEEDED = INCORRECT, CORRECT # for otp_needed
|
||||
|
||||
|
||||
class RadiusAuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RadiusStates(enum.IntEnum):
|
||||
NOT_CHECKED = -1
|
||||
INCORRECT = 0
|
||||
CORRECT = 1
|
||||
|
||||
# Aliases
|
||||
NOT_NEEDED = INCORRECT
|
||||
NEEDED = CORRECT
|
||||
|
||||
|
||||
class RadiusResult(typing.NamedTuple):
|
||||
"""
|
||||
Result of an AccessChallenge request.
|
||||
"""
|
||||
|
||||
pwd: RadiusStates = RadiusStates.INCORRECT
|
||||
replyMessage: typing.Optional[bytes] = None
|
||||
state: typing.Optional[bytes] = None
|
||||
otp: RadiusStates = RadiusStates.NOT_CHECKED
|
||||
otp_needed: RadiusStates = RadiusStates.NOT_CHECKED
|
||||
|
||||
|
||||
class RadiusClient:
|
||||
radiusServer: Client
|
||||
@ -68,13 +94,27 @@ class RadiusClient:
|
||||
dictionary: str = RADDICT,
|
||||
) -> None:
|
||||
self.radiusServer = Client(
|
||||
server=server, authport=authPort, secret=secret, dict=Dictionary(io.StringIO(dictionary))
|
||||
server=server,
|
||||
authport=authPort,
|
||||
secret=secret,
|
||||
dict=Dictionary(io.StringIO(dictionary)),
|
||||
)
|
||||
self.nasIdentifier = nasIdentifier
|
||||
self.appClassPrefix = appClassPrefix
|
||||
|
||||
# Second element of return value is the mfa code from field
|
||||
def authenticate(self, username: str, password: str, mfaField: str) -> typing.Tuple[typing.List[str], str]:
|
||||
def extractAccessChallenge(self, reply: pyrad.packet.AuthPacket) -> RadiusResult:
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.CORRECT,
|
||||
replyMessage=typing.cast(
|
||||
typing.List[bytes], reply.get('Reply-Message') or ['']
|
||||
)[0],
|
||||
state=typing.cast(typing.List[bytes], reply.get('State') or [b''])[0],
|
||||
otp_needed=RadiusStates.NEEDED,
|
||||
)
|
||||
|
||||
def sendAccessRequest(
|
||||
self, username: str, password: str, **kwargs
|
||||
) -> pyrad.packet.AuthPacket:
|
||||
req: pyrad.packet.AuthPacket = self.radiusServer.CreateAuthPacket(
|
||||
code=pyrad.packet.AccessRequest,
|
||||
User_Name=username,
|
||||
@ -83,9 +123,19 @@ class RadiusClient:
|
||||
|
||||
req["User-Password"] = req.PwCrypt(password)
|
||||
|
||||
reply = typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
|
||||
# Fill in extra fields
|
||||
for k, v in kwargs.items():
|
||||
req[k] = v
|
||||
|
||||
if reply.code != pyrad.packet.AccessAccept:
|
||||
return typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
|
||||
|
||||
# Second element of return value is the mfa code from field
|
||||
def authenticate(
|
||||
self, username: str, password: str, mfaField: str
|
||||
) -> typing.Tuple[typing.List[str], str]:
|
||||
reply = self.sendAccessRequest(username, password)
|
||||
|
||||
if reply.code not in (pyrad.packet.AccessAccept, pyrad.packet.AccessChallenge):
|
||||
raise RadiusAuthenticationError('Access denied')
|
||||
|
||||
# User accepted, extract groups...
|
||||
@ -93,7 +143,11 @@ class RadiusClient:
|
||||
groupClassPrefix = (self.appClassPrefix + 'group=').encode()
|
||||
groupClassPrefixLen = len(groupClassPrefix)
|
||||
if 'Class' in reply:
|
||||
groups = [i[groupClassPrefixLen:].decode() for i in typing.cast(typing.Iterable[bytes], reply['Class']) if i.startswith(groupClassPrefix)]
|
||||
groups = [
|
||||
i[groupClassPrefixLen:].decode()
|
||||
for i in typing.cast(typing.Iterable[bytes], reply['Class'])
|
||||
if i.startswith(groupClassPrefix)
|
||||
]
|
||||
else:
|
||||
logger.info('No "Class (25)" attribute found')
|
||||
return ([], '')
|
||||
@ -101,6 +155,99 @@ class RadiusClient:
|
||||
# ...and mfa code
|
||||
mfaCode = ''
|
||||
if mfaField and mfaField in reply:
|
||||
mfaCode = ''.join(i[groupClassPrefixLen:].decode() for i in typing.cast(typing.Iterable[bytes], reply['Class']) if i.startswith(groupClassPrefix))
|
||||
mfaCode = ''.join(
|
||||
i[groupClassPrefixLen:].decode()
|
||||
for i in typing.cast(typing.Iterable[bytes], reply['Class'])
|
||||
if i.startswith(groupClassPrefix)
|
||||
)
|
||||
return (groups, mfaCode)
|
||||
|
||||
def authenticate_only(self, username: str, password: str) -> RadiusResult:
|
||||
reply = self.sendAccessRequest(username, password)
|
||||
|
||||
if reply.code == pyrad.packet.AccessChallenge:
|
||||
return self.extractAccessChallenge(reply)
|
||||
|
||||
# user/pwd accepted: this user does not have challenge data
|
||||
if reply.code == pyrad.packet.AccessAccept:
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.CORRECT,
|
||||
otp_needed=RadiusStates.NOT_CHECKED,
|
||||
)
|
||||
|
||||
# user/pwd rejected
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.INCORRECT,
|
||||
)
|
||||
|
||||
def challenge_only(
|
||||
self, username: str, otp: str, state: bytes = b'0000000000000000'
|
||||
) -> RadiusResult:
|
||||
|
||||
# clean otp code
|
||||
otp = ''.join([x for x in otp if x in '0123456789'])
|
||||
|
||||
reply = self.sendAccessRequest(username, otp, State=state)
|
||||
|
||||
# correct OTP challenge
|
||||
if reply.code == pyrad.packet.AccessAccept:
|
||||
return RadiusResult(
|
||||
otp=RadiusStates.CORRECT,
|
||||
)
|
||||
|
||||
# incorrect OTP challenge
|
||||
return RadiusResult(
|
||||
otp=RadiusStates.INCORRECT,
|
||||
)
|
||||
|
||||
def authenticate_and_challenge(
|
||||
self, username: str, password: str, otp: str
|
||||
) -> RadiusResult:
|
||||
reply = self.sendAccessRequest(username, password)
|
||||
|
||||
if reply.code == pyrad.packet.AccessChallenge:
|
||||
state = typing.cast(typing.List[bytes], reply.get('State') or [b''])[0]
|
||||
replyMessage = typing.cast(
|
||||
typing.List[bytes], reply.get('Reply-Message') or ['']
|
||||
)[0]
|
||||
return self.challenge_only(username, otp, state=state)
|
||||
|
||||
# user/pwd accepted: but this user does not have challenge data
|
||||
# we should not be here...
|
||||
if reply.code == pyrad.packet.AccessAccept:
|
||||
logger.warning(
|
||||
"Radius OTP error: cheking for OTP for not needed user [%s]", username
|
||||
)
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.CORRECT,
|
||||
otp_needed=RadiusStates.NOT_NEEDED,
|
||||
)
|
||||
|
||||
# TODO: accept more AccessChallenge authentications (as RFC says)
|
||||
|
||||
# incorrect user/pwd
|
||||
return RadiusResult()
|
||||
|
||||
def authenticate_challenge(
|
||||
self, username: str, password: str = '', otp: str = '', state: bytes = b''
|
||||
) -> RadiusResult:
|
||||
'''
|
||||
wrapper for above 3 functions: authenticate_only, challenge_only, authenticate_and_challenge
|
||||
calls wrapped functions based on passed input values: (pwd/otp/state)
|
||||
'''
|
||||
# clean input data
|
||||
otp = ''.join([x for x in otp if x in '0123456789'])
|
||||
username = username.strip()
|
||||
password = password.strip()
|
||||
state = state.strip()
|
||||
|
||||
if not username or (not password and not otp):
|
||||
return RadiusResult() # no user/pwd provided
|
||||
|
||||
if not otp:
|
||||
return self.authenticate_only(username, password)
|
||||
if otp and not password:
|
||||
# check only otp with static/invented state. allow this ?
|
||||
return self.challenge_only(username, otp, state=b'0000000000000000')
|
||||
# otp and password
|
||||
return self.authenticate_and_challenge(username, password, otp)
|
||||
|
@ -85,7 +85,7 @@ class MFA(Module):
|
||||
# : override it in your own implementation.
|
||||
cacheTime: typing.ClassVar[int] = 5
|
||||
|
||||
class RESULT(enum.Enum):
|
||||
class RESULT(enum.IntEnum):
|
||||
"""
|
||||
This enum is used to know if the MFA code was sent or not.
|
||||
"""
|
||||
@ -143,7 +143,7 @@ class MFA(Module):
|
||||
def sendCode(self, request: 'ExtendedHttpRequest', userId: str, username: str, identifier: str, code: str) -> 'MFA.RESULT':
|
||||
"""
|
||||
This method will be invoked from "process" method, to send the MFA code to the user.
|
||||
If returns MFA.RESULT.VALID, the MFA code was sent.
|
||||
If returns MFA.RESULT.OK, the MFA code was sent.
|
||||
If returns MFA.RESULT.ALLOW, the MFA code was not sent, the user does not need to enter the MFA code.
|
||||
If raises an error, the MFA code was not sent, and the user needs to enter the MFA code.
|
||||
"""
|
||||
@ -156,7 +156,7 @@ class MFA(Module):
|
||||
The identifier where to send the code, will be obtained from "mfaIdentifier" method.
|
||||
Default implementation generates a random code and sends invokes "sendCode" method.
|
||||
|
||||
If returns MFA.RESULT.VALID, the MFA code was sent.
|
||||
If returns MFA.RESULT.OK, the MFA code was sent.
|
||||
If returns MFA.RESULT.ALLOW, the MFA code was not sent, the user does not need to enter the MFA code.
|
||||
If raises an error, the MFA code was not sent, and the user needs to enter the MFA code.
|
||||
"""
|
||||
|
@ -1 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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.
|
||||
|
||||
from . import mfa
|
||||
|
@ -1,3 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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 email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from re import T
|
||||
|
1
server/src/uds/mfas/Radius/__init__.py
Normal file
1
server/src/uds/mfas/Radius/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import mfa
|
318
server/src/uds/mfas/Radius/mfa.py
Normal file
318
server/src/uds/mfas/Radius/mfa.py
Normal file
@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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: Daniel Torregrosa
|
||||
"""
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
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.auths.Radius import client
|
||||
from uds.auths.Radius.client import NOT_CHECKED, INCORRECT, CORRECT, NOT_NEEDED, NEEDED
|
||||
from uds.core.auths.auth import webPassword
|
||||
from uds.core.auths import exceptions
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.module import Module
|
||||
from uds.core.util.request import ExtendedHttpRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RadiusOTP(mfas.MFA):
|
||||
'''
|
||||
Validates OTP challenge against a proper configured Radius Server with OTP
|
||||
using 'Access-Challenge' response from Radius Server [RFC2865, RFC5080]
|
||||
'''
|
||||
|
||||
typeName = _('Radius OTP Challenge')
|
||||
typeType = 'RadiusOTP'
|
||||
typeDescription = _('Radius OTP Challenge')
|
||||
iconFile = 'radius.png'
|
||||
cacheTime = 1 # In this MFA type there are not code generation nor sending... so ? 1 minute or too short ?
|
||||
|
||||
server = gui.TextField(
|
||||
length=64,
|
||||
label=_('Host'),
|
||||
order=1,
|
||||
tooltip=_('Radius Server IP or Hostname'),
|
||||
required=True,
|
||||
)
|
||||
port = gui.NumericField(
|
||||
length=5,
|
||||
label=_('Port'),
|
||||
defvalue='1812',
|
||||
order=2,
|
||||
tooltip=_('Radius authentication port (usually 1812)'),
|
||||
required=True,
|
||||
)
|
||||
secret = gui.TextField(
|
||||
length=64,
|
||||
label=_('Secret'),
|
||||
order=3,
|
||||
tooltip=_('Radius client secret'),
|
||||
required=True,
|
||||
)
|
||||
all_users_otp = gui.CheckBoxField(
|
||||
label=_('All users must send OTP'),
|
||||
order=4,
|
||||
defvalue=True,
|
||||
tooltip=_(
|
||||
'If unchecked, an authentication step is needed in order to know if this user must enter OTP. '
|
||||
'If checked, all users must enter OTP, so authentication step is skipped.'
|
||||
),
|
||||
)
|
||||
nasIdentifier = gui.TextField(
|
||||
length=64,
|
||||
label=_('NAS Identifier'),
|
||||
defvalue='uds-server',
|
||||
order=5,
|
||||
tooltip=_('NAS Identifier for Radius Server'),
|
||||
required=True,
|
||||
)
|
||||
|
||||
responseErrorAction = gui.ChoiceField(
|
||||
label=_('Radius OTP communication error action'),
|
||||
order=31,
|
||||
defaultValue='0',
|
||||
tooltip=_('Action for OTP server communication 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=_('Radius OTP networks'),
|
||||
rdonly=False,
|
||||
rows=5,
|
||||
order=32,
|
||||
tooltip=_('Networks for Radius OTP authentication'),
|
||||
required=False,
|
||||
tab=_('Config'),
|
||||
)
|
||||
|
||||
allowLoginWithoutMFA = gui.ChoiceField(
|
||||
label=_('User without defined OTP in server'),
|
||||
order=33,
|
||||
defaultValue='0',
|
||||
tooltip=_('Action for user without defined Radius Challenge'),
|
||||
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'),
|
||||
)
|
||||
|
||||
def initialize(self, values: 'Module.ValuesType') -> None:
|
||||
return super().initialize(values)
|
||||
|
||||
@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 radiusClient(self) -> client.RadiusClient:
|
||||
"""Return a new radius client ."""
|
||||
return client.RadiusClient(
|
||||
self.server.value,
|
||||
self.secret.value.encode(),
|
||||
authPort=self.port.num(),
|
||||
nasIdentifier=self.nasIdentifier.value,
|
||||
)
|
||||
|
||||
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 checkResult(self, action: str, request: 'ExtendedHttpRequest') -> mfas.MFA.RESULT:
|
||||
if self.checkAction(action, request):
|
||||
return mfas.MFA.RESULT.OK
|
||||
raise Exception('User not allowed to login')
|
||||
|
||||
def emptyIndentifierAllowedToLogin(self, request: 'ExtendedHttpRequest') -> bool:
|
||||
return self.checkAction(self.allowLoginWithoutMFA.value, request)
|
||||
|
||||
def label(self) -> str:
|
||||
return gettext('OTP Code')
|
||||
|
||||
def html(self, request: 'ExtendedHttpRequest') -> str:
|
||||
'''
|
||||
ToDo:
|
||||
- Maybe create a field in mfa definition to edit from admin panel ?
|
||||
- And/or add "Reply-Message" text from Radius Server response
|
||||
'''
|
||||
return gettext('Please enter OTP')
|
||||
|
||||
def process(
|
||||
self,
|
||||
request: 'ExtendedHttpRequest',
|
||||
userId: str,
|
||||
username: str,
|
||||
identifier: str,
|
||||
validity: typing.Optional[int] = None,
|
||||
) -> 'mfas.MFA.RESULT':
|
||||
'''
|
||||
check if this user must send OTP
|
||||
in order to check this, it is neccesary to first validate password (again) with radius server
|
||||
and get also radius State value (otp session)
|
||||
'''
|
||||
# if we are in a "all-users-otp" policy, avoid this step and go directly to ask for OTP
|
||||
if self.all_users_otp.value:
|
||||
return mfas.MFA.RESULT.OK
|
||||
|
||||
web_pwd = webPassword(request)
|
||||
try:
|
||||
connection = self.radiusClient()
|
||||
auth_reply = connection.authenticate_challenge(username, password=web_pwd)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Exception found connecting to Radius OTP %s: %s", e.__class__, e
|
||||
)
|
||||
if not self.checkAction(self.responseErrorAction.value, request):
|
||||
raise Exception(_('Radius OTP connection error'))
|
||||
logger.warning(
|
||||
"Radius OTP connection error: Allowing access to user [%s] from IP [%s] without OTP",
|
||||
username,
|
||||
request.ip,
|
||||
)
|
||||
return mfas.MFA.RESULT.ALLOWED
|
||||
|
||||
if auth_reply.pwd == INCORRECT:
|
||||
logger.warning(
|
||||
"Radius OTP error: User [%s] with invalid password from IP [%s]. Not synchronized password.",
|
||||
username,
|
||||
request.ip,
|
||||
)
|
||||
# we should not be here: not synchronized user password between auth server and radius server
|
||||
# What do we want to do here ??
|
||||
return self.checkResult(self.responseErrorAction.value, request)
|
||||
|
||||
if auth_reply.otp_needed == NOT_NEEDED:
|
||||
logger.warning(
|
||||
"Radius OTP error: User [%s] without OTP data from IP [%s]",
|
||||
username,
|
||||
request.ip,
|
||||
)
|
||||
return self.checkResult(self.allowLoginWithoutMFA.value, request)
|
||||
|
||||
# correct password and otp_needed
|
||||
return mfas.MFA.RESULT.OK
|
||||
|
||||
def validate(
|
||||
self,
|
||||
request: 'ExtendedHttpRequest',
|
||||
userId: str,
|
||||
username: str,
|
||||
identifier: str,
|
||||
code: str,
|
||||
validity: typing.Optional[int] = None,
|
||||
) -> None:
|
||||
'''
|
||||
Validate the OTP code
|
||||
|
||||
we could have saved state+replyMessage in ddbb at "process" step and reuse it here
|
||||
but finally it is a lot easier to generate new one on each otp try
|
||||
otherwise we need to redirect to username/password form in each otp try in order to
|
||||
regenerate a new State after a wrong sent OTP code
|
||||
slightly less efficient but a lot simpler
|
||||
'''
|
||||
|
||||
try:
|
||||
err = _('Invalid OTP code')
|
||||
|
||||
web_pwd = webPassword(request)
|
||||
try:
|
||||
connection = self.radiusClient()
|
||||
auth_reply = connection.authenticate_challenge(
|
||||
username, password=web_pwd, otp=code
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Exception found connecting to Radius OTP %s: %s", e.__class__, e
|
||||
)
|
||||
if not self.checkAction(self.responseErrorAction.value, request):
|
||||
raise Exception(_('Radius OTP connection error'))
|
||||
logger.warning(
|
||||
"Radius OTP connection error: Allowing access to user [%s] from IP [%s] without OTP",
|
||||
username,
|
||||
request.ip,
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug("otp auth_reply: %s", auth_reply)
|
||||
if auth_reply.otp == CORRECT:
|
||||
logger.warning(
|
||||
"Radius OTP correctly logged in: Allowing access to user [%s] from IP [%s] with correct OTP",
|
||||
username,
|
||||
request.ip,
|
||||
)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
# Any error means invalid code
|
||||
err = str(e)
|
||||
|
||||
logger.warning(
|
||||
"Radius OTP error: Denying access to user [%s] from IP [%s] with incorrect OTP",
|
||||
username,
|
||||
request.ip,
|
||||
)
|
||||
raise exceptions.MFAError(err)
|
BIN
server/src/uds/mfas/Radius/radius.png
Normal file
BIN
server/src/uds/mfas/Radius/radius.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
@ -1 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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 mfa
|
||||
|
@ -1,3 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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
|
||||
"""
|
||||
|
||||
import typing
|
||||
import re
|
||||
import logging
|
||||
|
@ -1 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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 mfa
|
||||
|
@ -1,4 +1,34 @@
|
||||
from re import T
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 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. 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
|
||||
"""
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -12,7 +11,7 @@
|
||||
# * 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. nor the names of its contributors
|
||||
# * 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.
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user