forked from shaba/openuds
Added radius authenticator for UDS
This commit is contained in:
parent
5f8abdfa41
commit
394ceb9e66
39
server/src/uds/auths/Radius/__init__.py
Normal file
39
server/src/uds/auths/Radius/__init__.py
Normal 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'
|
181
server/src/uds/auths/Radius/authenticator.py
Normal file
181
server/src/uds/auths/Radius/authenticator.py
Normal 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')]
|
101
server/src/uds/auths/Radius/client.py
Normal file
101
server/src/uds/auths/Radius/client.py
Normal 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
|
||||
|
BIN
server/src/uds/auths/Radius/radius.png
Normal file
BIN
server/src/uds/auths/Radius/radius.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user