Added radius authenticator for UDS

This commit is contained in:
Adolfo Gómez García 2021-06-01 12:41:58 +02:00
parent 5f8abdfa41
commit 394ceb9e66
5 changed files with 325 additions and 1 deletions

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# 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.
"""
Sample authenticator. We import here the module, and uds.auths module will
take care of registering it as provider
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from .authenticator import RadiusAuth
__updated__ = '2014-02-19'

View File

@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 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.U. 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.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
from django.utils.translation import ugettext_noop as _
from uds.core.ui import gui
from uds.core import auths
from uds.core.managers import cryptoManager
from . import client
if typing.TYPE_CHECKING:
from django.http import (
HttpRequest
)
logger = logging.getLogger(__name__)
class RadiusAuth(auths.Authenticator):
"""
UDS Radius authenticator
"""
typeName = _('Radius Authenticator')
typeType = 'RadiusAuthenticator'
typeDescription = _('Radius Authenticator')
iconFile = 'radius.png'
userNameLabel = _('User')
groupNameLabel = _('Group')
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,
)
nasIdentifier = gui.TextField(
length=64,
label=_('NAS Identifier'),
defvalue='uds-server',
order=10,
tooltip=_('NAS Identifier for Radius Server'),
required=True,
tab=gui.ADVANCED_TAB,
)
appClassPrefix = gui.TextField(
length=64,
label=_('App Prefix for Class Attributes'),
defvalue='',
order=11,
tooltip=_('Application prefix for filtering groups from "Class" attribute'),
tab=gui.ADVANCED_TAB,
)
globalGroup = gui.TextField(
length=64,
label=_('Global group'),
defvalue='',
order=12,
tooltip=_('If set, this value will be added as group for all radius users'),
tab=gui.ADVANCED_TAB,
)
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
pass
def radiusClient(self) -> client.RadiusClient:
return client.RadiusClient(
self.server.value,
self.secret.value.encode(),
authPort=self.port.num(),
nasIdentifier=self.nasIdentifier.value,
)
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
) -> bool:
try:
connection = self.radiusClient()
groups = connection.authenticate(username=username, password=credentials)
except Exception:
return False
if self.globalGroup.value.strip():
groups.append(self.globalGroup.value.strip())
# Cache groups for "getGroups", because radius will not send us those
with self.storage.map() as storage:
storage[username] = groups
# Validate groups
groupsManager.validate(groups)
return True
def getGroups(self, username: str, groupsManager: 'auths.GroupsManager') -> None:
with self.storage.map() as storage:
groupsManager.validate(storage.get(username, []))
def createUser(self, usrData: typing.Dict[str, str]) -> None:
pass
def removeUser(self, username: str) -> None:
with self.storage.map() as storage:
if username in storage:
del storage[username]
return super().removeUser(username)
@staticmethod
def test(env, data):
try:
auth = RadiusAuth(None, env, data) # type: ignore
return auth.testConnection()
except Exception as e:
logger.error(
"Exception found testing Radius auth %s: %s", e.__class__, e
)
return [False, _('Error testing connection')]
def testConnection(self): # pylint: disable=too-many-return-statements
try:
connection = self.radiusClient()
# Reply is not important...
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10))
except client.RadiusAuthenticationError as e:
pass
except Exception:
logger.exception('Connecting')
return [False, _('Connection to Radius server failed')]
return [True, _('Connection to Radius server seems ok')]

View File

@ -0,0 +1,101 @@
import io
import logging
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
ATTRIBUTE User-Password 2 string
ATTRIBUTE CHAP-Password 3 octets
ATTRIBUTE NAS-IP-Address 4 ipaddr
ATTRIBUTE NAS-Port 5 integer
ATTRIBUTE Service-Type 6 integer
ATTRIBUTE Framed-Protocol 7 integer
ATTRIBUTE Framed-IP-Address 8 ipaddr
ATTRIBUTE Framed-IP-Netmask 9 ipaddr
ATTRIBUTE Framed-Routing 10 integer
ATTRIBUTE Filter-Id 11 string
ATTRIBUTE Framed-MTU 12 integer
ATTRIBUTE Framed-Compression 13 integer
ATTRIBUTE Login-IP-Host 14 ipaddr
ATTRIBUTE Login-Service 15 integer
ATTRIBUTE Login-TCP-Port 16 integer
ATTRIBUTE Reply-Message 18 string
ATTRIBUTE Callback-Number 19 string
ATTRIBUTE Callback-Id 20 string
ATTRIBUTE Framed-Route 22 string
ATTRIBUTE Framed-IPX-Network 23 ipaddr
ATTRIBUTE State 24 octets
ATTRIBUTE Class 25 octets
ATTRIBUTE Vendor-Specific 26 octets
ATTRIBUTE Session-Timeout 27 integer
ATTRIBUTE Idle-Timeout 28 integer
ATTRIBUTE Termination-Action 29 integer
ATTRIBUTE Called-Station-Id 30 string
ATTRIBUTE Calling-Station-Id 31 string
ATTRIBUTE NAS-Identifier 32 string
ATTRIBUTE Proxy-State 33 octets
ATTRIBUTE Login-LAT-Service 34 string
ATTRIBUTE Login-LAT-Node 35 string
ATTRIBUTE Login-LAT-Group 36 octets
ATTRIBUTE Framed-AppleTalk-Link 37 integer
ATTRIBUTE Framed-AppleTalk-Network 38 integer
ATTRIBUTE Framed-AppleTalk-Zone 39 string"""
class RadiusClient:
radiusServer: Client
nasIdentifier: str
appClassPrefix: str
def __init__(
self,
server: str,
secret: bytes,
*,
authPort: int = 1812,
nasIdentifier: str = 'uds-server',
appClassPrefix: str = '',
dictionary: str = RADDICT,
) -> None:
self.radiusServer = Client(
server=server, authport=authPort, secret=secret, dict=Dictionary(io.StringIO(dictionary))
)
self.nasIdentifier = nasIdentifier
self.appClassPrefix = appClassPrefix
def authenticate(self, username: str, password: str) -> typing.List[str]:
req: pyrad.packet.AuthPacket = self.radiusServer.CreateAuthPacket(
code=pyrad.packet.AccessRequest,
User_Name=username,
NAS_Identifier=self.nasIdentifier,
)
req["User-Password"] = req.PwCrypt(password)
reply = typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
if reply.code != pyrad.packet.AccessAccept:
raise RadiusAuthenticationError('Access denied')
# User accepted, extract groups...
# All radius users belongs to, at least, 'uds-users' group
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)]
else:
logger.info('No "Class (25)" attribute found')
return []
return groups

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -165,7 +165,7 @@ class StorageAsDict(MutableMapping):
return self._filtered.filter(key=self._key(key)).exists()
return False
def __len__(self):
def __len__(self) -> int:
return self._filtered.count()
# Optimized methods, avoid re-reading from DB
@ -175,6 +175,9 @@ class StorageAsDict(MutableMapping):
def values(self):
return iter(_decodeValue(i.key, i.data)[1] for i in self._filtered)
def get(self, key: str, default: typing.Any = None) -> typing.Any:
return self[key] or default
# Custom utility methods
@property
def group(self) -> str: