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

This commit is contained in:
Adolfo Gómez García 2022-08-23 15:23:25 +02:00
commit 78c7039c54
12 changed files with 666 additions and 19 deletions

View File

@ -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)

View File

@ -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.
"""

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
from . import mfa

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.
#