From 8e370a15d9a39f24d9bc7225e2f0a5ca8bca14dc Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:26:54 +0200 Subject: [PATCH 01/22] merge --- api/permissions.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/api/permissions.py b/api/permissions.py index 8e3bd2d4..9f120bec 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -63,7 +63,7 @@ def _get_param_in_view(view, param_name): "cannot apply {} on a view that does not set " "`.{}` or have a `.get_{}()` method." ).format( - self.__class__.__name__, param_name, param_name + view.__class__.__name__, param_name, param_name ) if hasattr(view, "get_" + param_name): @@ -213,7 +213,7 @@ class AutodetectACLPermission(permissions.BasePermission): return [perm(obj) for perm in self.perms_obj_map[method]] - @staticmethod + @ staticmethod def _queryset(view): return _get_param_in_view(view, "queryset") @@ -240,9 +240,7 @@ class AutodetectACLPermission(permissions.BasePermission): if getattr(view, "_ignore_model_permissions", False): return True - # Bypass permission verifications if it is a functional view - # (permissions are handled by ACL) - if not hasattr(view, "queryset") and not hasattr(view, "get_queryset"): + if not getattr(view, "queryset", None): return True if not request.user or not request.user.is_authenticated: @@ -279,7 +277,8 @@ class AutodetectACLPermission(permissions.BasePermission): # they have read permissions to see 403, or not, and simply see # a 404 response. - SAFE_METHODS = ("GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE") + SAFE_METHODS = ("GET", "OPTIONS", "HEAD", + "POST", "PUT", "PATCH", "DELETE") if request.method in SAFE_METHODS: # Read permissions already checked and failed, no need From 8ba9fa2fa0639ce825e052ddfd164305ef044d10 Mon Sep 17 00:00:00 2001 From: chapeau Date: Sat, 28 Nov 2020 12:09:36 +0100 Subject: [PATCH 02/22] premiere version de l'api radius --- radius/__init__.py | 0 radius/api/__init__.py | 0 radius/api/serializers.py | 98 ++++++++++++++++++++++++++++ radius/api/urls.py | 35 ++++++++++ radius/api/views.py | 133 ++++++++++++++++++++++++++++++++++++++ radius/urls.py | 4 ++ radius/views.py | 0 7 files changed, 270 insertions(+) create mode 100644 radius/__init__.py create mode 100644 radius/api/__init__.py create mode 100644 radius/api/serializers.py create mode 100644 radius/api/urls.py create mode 100644 radius/api/views.py create mode 100644 radius/urls.py create mode 100644 radius/views.py diff --git a/radius/__init__.py b/radius/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/radius/api/__init__.py b/radius/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/radius/api/serializers.py b/radius/api/serializers.py new file mode 100644 index 00000000..41959eb9 --- /dev/null +++ b/radius/api/serializers.py @@ -0,0 +1,98 @@ +from rest_framework import serializers + +import machines.models as machines +import users.models as users +from api.serializers import NamespacedHMSerializer +from rest_framework.serializers import Serializer + + +class InterfaceSerializer(Serializer): + mac_address = serializers.CharField() + ipv4 = serializers.CharField(source="ipv4.ipv4") + active = serializers.BooleanField(source="is_active") + user_pk = serializers.CharField(source="machine.user.pk") + # machine_type_pk = serializers.CharField(source="machine_type.pk") + # switch_stack = serializers.CharField(source="machine.switch.stack") + machine_short_name = serializers.CharField(source="machine.short_name") + is_ban = serializers.BooleanField(source="machine.user.is_ban") + vlan_id = serializers.IntegerField( + source="machine_type.ip_type.vlan.vlan_id") + + +class NasSerializer(Serializer): + port_access_mode = serializers.CharField() + autocapture_mac = serializers.BooleanField() + + +class UserSerializer(Serializer): + access = serializers.BooleanField(source="has_access") + pk = serializers.CharField() + pwd_ntlm = serializers.CharField() + state = serializers.CharField() + email_state = serializers.IntegerField() + is_ban = serializers.BooleanField() + is_connected = serializers.BooleanField() + is_whitelisted = serializers.BooleanField() + + +class PortSerializer(Serializer): + state = serializers.BooleanField() + room = serializers.CharField() + + +class VlanSerializer(Serializer): + vlan_id = serializers.IntegerField() + + +class PortProfileSerializer(Serializer): + vlan_untagged = VlanSerializer() + radius_type = serializers.CharField() + + +class SwitchSerializer(Serializer): + name = serializers.CharField(source="short_name") + ipv4 = serializers.CharField() + + +class RadiusAttributeSerializer(Serializer): + attribute = serializers.CharField() + value = serializers.CharField() + + +class RadiusOptionSerializer(Serializer): + radius_general_policy = serializers.CharField() + unknown_machine = serializers.CharField() + unknown_machine_vlan = VlanSerializer() + unknown_machine_attributes = RadiusAttributeSerializer(many=True) + unknown_port = serializers.CharField() + unknown_port_vlan = VlanSerializer() + unknown_port_attributes = RadiusAttributeSerializer(many=True) + unknown_room = serializers.CharField() + unknown_room_vlan = VlanSerializer() + unknown_room_attributes = RadiusAttributeSerializer(many=True) + non_member = serializers.CharField() + non_member_vlan = VlanSerializer() + non_member_attributes = RadiusAttributeSerializer(many=True) + banned = serializers.CharField() + banned_vlan = VlanSerializer() + banned_attributes = RadiusAttributeSerializer(many=True) + vlan_decision_ok = VlanSerializer() + ok_attributes = RadiusAttributeSerializer(many=True) + + +class AuthorizeResponseSerializer(Serializer): + nas = NasSerializer(read_only=True) + user = UserSerializer(read_only=True) + user_interface = InterfaceSerializer(read_only=True) + + +class PostAuthResponseSerializer(Serializer): + nas = NasSerializer(read_only=True) + room_users = UserSerializer(many=True) + port = PortSerializer() + port_profile = PortProfileSerializer(partial=True) + switch = SwitchSerializer() + user_interface = InterfaceSerializer() + radius_option = RadiusOptionSerializer() + EMAIL_STATE_UNVERIFIED = serializers.IntegerField() + RADIUS_OPTION_REJECT = serializers.CharField() diff --git a/radius/api/urls.py b/radius/api/urls.py new file mode 100644 index 00000000..af532a5b --- /dev/null +++ b/radius/api/urls.py @@ -0,0 +1,35 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2020 Corentin Canebier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from . import views + +urls_view = [ + # (r"radius/nas-interface-from-id/(?P.+)$", views.nas_from_id_view), + # (r"radius/nas-from-machine-type/(?P.+)$", views.NasFromMachineTypeView), + # (r"radius/user-from-username/(?P.+)$", views.UserFromUsernameView), + # (r"radius/interface-from-mac-address/(?P.+)$", views.InterfaceFromMacAddressView), +] +urls_functional_view = [ + (r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F:\-]{17})$", + views.authorize, None), + (r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F:\-]{17})$", + views.post_auth, None), +] diff --git a/radius/api/views.py b/radius/api/views.py new file mode 100644 index 00000000..7c16f916 --- /dev/null +++ b/radius/api/views.py @@ -0,0 +1,133 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2020 Corentin Canebier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from rest_framework.decorators import api_view +from rest_framework.response import Response +from django.db.models import Q + +from . import serializers +from machines.models import Domain, IpList, Interface, Nas +from users.models import User +from preferences.models import RadiusOption +from topologie.models import Port, Switch + + +class AuthorizeResponse: + def __init__(self, nas, user, user_interface): + self.nas = nas + self.user = user + self.user_interface = user_interface + + def can_view(self, user): + return [True] + + +@api_view(['GET']) +def authorize(request, nas_id, username, mac_address): + + nas_interface = Interface.objects.filter( + Q(domain=Domain.objects.filter(name=nas_id)) + | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + ).first() + nas_type = None + if nas_interface: + nas_type = Nas.objects.filter( + nas_type=nas_interface.machine_type).first() + + user = User.objects.filter(pseudo__iexact=username).first() + user_interface = Interface.objects.filter(mac_address=mac_address).first() + + serialized = serializers.AuthorizeResponseSerializer( + AuthorizeResponse(nas_type, user, user_interface)) + + return Response(data=serialized.data) + + +class PostAuthResponse: + def __init__(self, nas, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT): + self.nas = nas + self.room_users = room_users + self.port = port + self.port_profile = port_profile + self.switch = switch + self.user_interface = user_interface + self.radius_option = radius_option + self.EMAIL_STATE_UNVERIFIED = EMAIL_STATE_UNVERIFIED + self.RADIUS_OPTION_REJECT = RADIUS_OPTION_REJECT + + def can_view(self, user): + return [True] + + +@api_view(['GET']) +def post_auth(request, nas_id, nas_port, user_mac): + # get nas_type + nas_interface = Interface.objects.prefetch_related("machine__switch__stack").filter( + Q(domain=Domain.objects.filter(name=nas_id)) + | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + ).first() + print(nas_id) + nas_type = None + if nas_interface: + nas_type = Nas.objects.filter( + nas_type=nas_interface.machine_type).first() + + # get switch + switch = Switch.objects.filter(machine_ptr=nas_interface.machine).first() + if hasattr(nas_interface.machine, "switch"): + stack = nas_interface.machine.switch.stack + if stack: + id_stack_member = nas_port.split("-")[1].split("/")[0] + switch = ( + Switch.objects.filter(stack=stack) + .filter(stack_member_id=id_stack_member) + .first() + ) + + # get port + port_number = nas_port.split(".")[0].split("/")[-1][-2:] + port = Port.objects.filter(switch=switch, port=port_number).first() + + port_profile = port.get_port_profile + + # get user_interface + user_interface = ( + Interface.objects.filter(mac_address=user_mac) + .select_related("machine__user") + .select_related("ipv4") + .first() + ) + + # get room users + room_users = User.objects.filter( + Q(club__room=port.room) | Q(adherent__room=port.room) + ) + + # get radius options + radius_option = RadiusOption.objects.first() + print(radius_option) + + EMAIL_STATE_UNVERIFIED = User.EMAIL_STATE_UNVERIFIED + RADIUS_OPTION_REJECT = RadiusOption.REJECT + serialized = serializers.PostAuthResponseSerializer( + PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT)) + + return Response(data=serialized.data) diff --git a/radius/urls.py b/radius/urls.py new file mode 100644 index 00000000..4271f701 --- /dev/null +++ b/radius/urls.py @@ -0,0 +1,4 @@ +from . import views + +urlpatterns = [] + diff --git a/radius/views.py b/radius/views.py new file mode 100644 index 00000000..e69de29b From 01e7822d95fc5a816e716200a3d50fa4c6335d82 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:28:56 +0200 Subject: [PATCH 03/22] merge --- freeradius_utils/auth.py | 388 +++++++++++++++----------------------- radius/api/serializers.py | 30 ++- radius/api/urls.py | 12 +- radius/api/views.py | 44 +++-- 4 files changed, 211 insertions(+), 263 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 00c3e774..4c507f62 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -1,5 +1,5 @@ # -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # @@ -7,6 +7,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Lara Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2020 Corentin Canebier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -59,20 +60,18 @@ from preferences.models import RadiusOption from topologie.models import Port, Switch from users.models import User - -# Logging class RadiusdHandler(logging.Handler): - """Logs handler for freeradius""" + """Handler de logs pour freeradius""" def emit(self, record): - """Log message processing, level are converted""" + """Process un message de log, en convertissant les niveaux""" if record.levelno >= logging.WARN: rad_sig = radiusd.L_ERR elif record.levelno >= logging.INFO: rad_sig = radiusd.L_INFO else: rad_sig = radiusd.L_DBG - radiusd.radlog(rad_sig, str(record.msg)) + radiusd.radlog(rad_sig, record.msg.encode("utf-8")) # Init for logging @@ -85,15 +84,17 @@ logger.addHandler(handler) def radius_event(fun): - """Decorator for freeradius fonction with radius. - This function take a unique argument which is a list of tuples (key, value) - and return a tuple of 3 values which are: - * return code (see radiusd.RLM_MODULE_* ) - * a tuple of 2 elements for response value (access ok , etc) - * a tuple of 2 elements for internal value to update (password for example) + """Décorateur pour les fonctions d'interfaces avec radius. + Une telle fonction prend un uniquement argument, qui est une liste de + tuples (clé, valeur) et renvoie un triplet dont les composantes sont : + * le code de retour (voir radiusd.RLM_MODULE_* ) + * un tuple de couples (clé, valeur) pour les valeurs de réponse (accès ok + et autres trucs du genre) + * un tuple de couples (clé, valeur) pour les valeurs internes à mettre à + jour (mot de passe par exemple) - Here, we convert the list of tuples into a dictionnary. - """ + On se contente avec ce décorateur (pour l'instant) de convertir la liste de + tuples en entrée en un dictionnaire.""" def new_f(auth_data): """ The function transforming the tuples as dict """ @@ -106,12 +107,15 @@ def radius_event(fun): # Ex: Calling-Station-Id: "une_adresse_mac" data[key] = value.replace('"', "") try: + # TODO s'assurer ici que les tuples renvoy s sont bien des + # (str,str) : rlm_python ne dig re PAS les unicodes return fun(data) except Exception as err: exc_type, exc_instance, exc_traceback = sys.exc_info() formatted_traceback = "".join(traceback.format_tb(exc_traceback)) logger.error("Failed %r on data %r" % (err, auth_data)) - logger.error("Function %r, Traceback : %r" % (fun, formatted_traceback)) + logger.error("Function %r, Traceback : %r" % + (fun, formatted_traceback)) return radiusd.RLM_MODULE_FAIL return new_f @@ -122,6 +126,18 @@ def instantiate(*_): """Usefull for instantiate ldap connexions otherwise, do nothing""" logger.info("Instantiation") + path = (os.path.dirname(os.path.abspath(__file__))) + + config = ConfigParser() + config.read(path+'/config.ini') + + api_hostname = config.get('Re2o', 'hostname') + api_password = config.get('Re2o', 'password') + api_username = config.get('Re2o', 'username') + + global api_client + api_client = Re2oAPIClient( + api_hostname, api_username, api_password, use_tls=True) @radius_event @@ -171,32 +187,18 @@ def post_auth(data): return radiusd.RLM_MODULE_OK nas_type = Nas.objects.filter(nas_type=nas_instance.machine_type).first() if not nas_type: - logger.info("This kind of nas is not registered in the database!") + logger.info("Proxified request, nas unknown") return radiusd.RLM_MODULE_OK - mac = data.get("Calling-Station-Id", None) + # If it is a switch + if switch: + sw_name = switch["name"] or "?" + room = "Unknown port" + if port: + room = port.room or "Unknown room" - # Switchs and access point can have several interfaces - nas_machine = nas_instance.machine - # If it is a switchs - if hasattr(nas_machine, "switch"): - port = data.get("NAS-Port-Id", data.get("NAS-Port", None)) - # If the switch is part of a stack, calling ip is different from calling switch. - instance_stack = nas_machine.switch.stack - if instance_stack: - # If it is a stack, we select the correct switch in the stack - id_stack_member = port.split("-")[1].split("/")[0] - nas_machine = ( - Switch.objects.filter(stack=instance_stack) - .filter(stack_member_id=id_stack_member) - .prefetch_related("interface_set__domain__extension") - .first() - ) - # Find the port number from freeradius, works both with HP, Cisco - # and juniper output - port = port.split(".")[0].split("/")[-1][-2:] - out = decide_vlan_switch(nas_machine, nas_type, port, mac) - sw_name, room, reason, vlan_id, decision, attributes = out + out = decide_vlan_switch(data_from_api, mac, nas_port) + reason, vlan_id, decision, attributes = out if decision: log_message = "(wired) %s -> %s [%s%s]" % ( @@ -232,70 +234,50 @@ def post_auth(data): return radiusd.RLM_MODULE_OK -# TODO : remove this function -@radius_event -def dummy_fun(_): - """Do nothing, successfully. """ - return radiusd.RLM_MODULE_OK - - -def detach(_=None): - """Detatch the auth""" - print("*** goodbye from auth.py ***") - return radiusd.RLM_MODULE_OK - - -def find_nas_from_request(nas_id): - """ Get the nas object from its ID """ - nas = ( - Interface.objects.filter( - Q(domain=Domain.objects.filter(name=nas_id)) - | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) - ) - .select_related("machine_type") - .select_related("machine__switch__stack") - ) - return nas.first() - - -def check_user_machine_and_register(nas_type, username, mac_address): +def check_user_machine_and_register(nas_type, user, user_interface): """Check if username and mac are registered. Register it if unknown. Return the user ntlm password if everything is ok. Used for 802.1X auth""" - interface = Interface.objects.filter(mac_address=mac_address).first() - user = User.objects.filter(pseudo__iexact=username).first() if not user: return (False, "User unknown", "") - if not user.has_access(): + if not user["access"]: return (False, "Invalid connexion (non-contributing user)", "") - if interface: - if interface.machine.user != user: + if user_interface: + if user_interface["user_pk"] != user["pk"]: return ( False, "Mac address registered on another user account", "", ) - elif not interface.is_active: + elif not user_interface["active"]: return (False, "Interface/Machine disabled", "") - elif not interface.ipv4: - interface.assign_ipv4() - return (True, "Ok, new ipv4 assignement...", user.pwd_ntlm) + elif not user_interface["ipv4"]: + # interface.assign_ipv4() + return (True, "Ok, new ipv4 assignement...", user.get("pwd_ntlm", "")) else: - return (True, "Access ok", user.pwd_ntlm) + return (True, "Access ok", user.get("pwd_ntlm", "")) elif nas_type: - if nas_type.autocapture_mac: - result, reason = user.autoregister_machine(mac_address, nas_type) - if result: - return (True, "Access Ok, Registering mac...", user.pwd_ntlm) - else: - return (False, "Error during mac register %s" % reason, "") + if nas_type["autocapture_mac"]: + # result, reason = user.autoregister_machine(mac_address, nas_type) + # if result: + # return (True, "Access Ok, Registering mac...", user.pwd_ntlm) + # else: + # return (False, "Error during mac register %s" % reason, "") + return (False, "L'auto capture est désactivée", "") else: return (False, "Unknown interface/machine", "") else: return (False, "Unknown interface/machine", "") -def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): +def set_radius_attributes_values(attributes, values): + return ( + (str(attribute.attribute), str(attribute.value % values)) + for attribute in attributes + ) + + +def decide_vlan_switch(data_from_api, user_mac, nas_port): """Function for selecting vlan for a switch with wired mac auth radius. Several modes are available : - all modes: @@ -328,10 +310,24 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): - decision (bool) - Other Attributs (attribut:str, operator:str, value:str) """ + + nas_type = data_from_api["nas"] + room_users = data_from_api["room_users"] + port = data_from_api["port"] + port_profile = data_from_api["port_profile"] + switch = data_from_api["switch"] + user_interface = data_from_api["user_interface"] + radius_option = data_from_api["radius_option"] + EMAIL_STATE_UNVERIFIED = data_from_api["EMAIL_STATE_UNVERIFIED"] + RADIUS_OPTION_REJECT = data_from_api["RADIUS_OPTION_REJECT"] + USER_STATE_ACTIVE = data_from_api["USER_STATE_ACTIVE"] + attributes_kwargs = { - "client_mac": str(mac_address), - "switch_port": str(port_number), + "client_mac": str(user_mac), + "switch_port": str(nas_port.split(".")[0].split("/")[-1][-2:]), + "switch_ip": str(switch.ipv4) } + # Get port from switch and port number extra_log = "" # If NAS is unknown, go to default vlan @@ -353,39 +349,35 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): # If the port is unknwon, go to default vlan # We don't have enought information to make a better decision - if not port: + if not port or not port_profile: return ( - sw_name, "Unknown port", - "PUnknown port", - getattr( - RadiusOption.get_cached_value("unknown_port_vlan"), "vlan_id", None - ), - RadiusOption.get_cached_value("unknown_port") != RadiusOption.REJECT, - RadiusOption.get_attributes("unknown_port_attributes", attributes_kwargs), + radius_option["unknown_port_vlan"] and radius_option["unknown_port_vlan"]["vlan_id"] or None, + radius_option["unknown_port"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["unknown_port_attributes"], attributes_kwargs), ) # Retrieve port profile port_profile = port.get_port_profile # If a vlan is precised in port config, we use it - if port_profile.vlan_untagged: - DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) + if port_profile["vlan_untagged"]: + DECISION_VLAN = int(port_profile["vlan_untagged"]["vlan_id"]) extra_log = "Force sur vlan " + str(DECISION_VLAN) attributes = () else: - DECISION_VLAN = RadiusOption.get_cached_value("vlan_decision_ok").vlan_id - attributes = RadiusOption.get_attributes("ok_attributes", attributes_kwargs) + DECISION_VLAN = radius_option["vlan_decision_ok"]["vlan_id"] + attributes = set_radius_attributes_values( + radius_option["ok_attributes"], attributes_kwargs) # If the port is disabled in re2o, REJECT - if not port.state: - return (sw_name, port.room, "Port disabled", None, False, ()) + if not port["state"]: + return ("Port disabled", None, False, ()) # If radius is disabled, decision is OK - if port_profile.radius_type == "NO": + if port_profile["radius_type"] == "NO": return ( - sw_name, - "", "No Radius auth enabled on this port" + extra_log, DECISION_VLAN, True, @@ -394,11 +386,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): # If 802.1X is enabled, people has been previously accepted. # Go to the decision vlan - if (nas_type.port_access_mode, port_profile.radius_type) == ("802.1X", "802.1X"): - room = port.room or "Room unknown" + if (nas_type["port_access_mode"], port_profile["radius_type"]) == ("802.1X", "802.1X"): return ( - sw_name, - room, "Accept authentication 802.1X", DECISION_VLAN, True, @@ -409,166 +398,101 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): # If strict mode is enabled, we check every user related with this port. If # one user or more is not enabled, we reject to prevent from sharing or # spoofing mac. - if port_profile.radius_mode == "STRICT": - room = port.room - if not room: + if port_profile["radius_mode"] == "STRICT": + if not port["room"]: return ( - sw_name, - "Unknown", "Unkwown room", - getattr( - RadiusOption.get_cached_value("unknown_room_vlan"), "vlan_id", None - ), - RadiusOption.get_cached_value("unknown_room") != RadiusOption.REJECT, - RadiusOption.get_attributes( - "unknown_room_attributes", attributes_kwargs - ), + radius_option["unknown_room_vlan"] and radius_option["unknown_room_vlan"]["vlan_id"] or None, + radius_option["unknown_room"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["unknown_room_attributes"], attributes_kwargs), ) - room_user = User.objects.filter( - Q(club__room=port.room) | Q(adherent__room=port.room) - ) - if not room_user: + if not room_users: return ( - sw_name, - room, "Non-contributing room", - getattr( - RadiusOption.get_cached_value("non_member_vlan"), "vlan_id", None - ), - RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, - RadiusOption.get_attributes("non_member_attributes", attributes_kwargs), + radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, + radius_option["non_member"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["non_member_attributes"], attributes_kwargs), + ) + + all_user_ban = True + at_least_one_active_user = False + + for user in room_users: + if not user["is_ban"] and user["state"] == USER_STATE_ACTIVE: + all_user_ban = False + elif user["email_state"] != EMAIL_STATE_UNVERIFIED and (user["is_connected"] or user["is_whitelisted"]): + at_least_one_active_user = True + + if all_user_ban: + return ( + "User is banned or disabled", + radius_option["banned_vlan"] and radius_option["banned_vlan"]["vlan_id"] or None, + radius_option["banned"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["banned_attributes"], attributes_kwargs), + ) + if not at_least_one_active_user: + return ( + "Non-contributing member or unconfirmed mail", + radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, + radius_option["non_member"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["non_member_attributes"], attributes_kwargs), ) - for user in room_user: - if user.is_ban() or user.state != User.STATE_ACTIVE: - return ( - sw_name, - room, - "User is banned or disabled", - getattr( - RadiusOption.get_cached_value("banned_vlan"), "vlan_id", None - ), - RadiusOption.get_cached_value("banned") != RadiusOption.REJECT, - RadiusOption.get_attributes("banned_attributes", attributes_kwargs), - ) - elif user.email_state == User.EMAIL_STATE_UNVERIFIED: - return ( - sw_name, - room, - "User is suspended (mail has not been confirmed)", - getattr( - RadiusOption.get_cached_value("non_member_vlan"), - "vlan_id", - None, - ), - RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, - RadiusOption.get_attributes( - "non_member_attributes", attributes_kwargs - ), - ) - elif not (user.is_connected() or user.is_whitelisted()): - return ( - sw_name, - room, - "Non-contributing member", - getattr( - RadiusOption.get_cached_value("non_member_vlan"), - "vlan_id", - None, - ), - RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, - RadiusOption.get_attributes( - "non_member_attributes", attributes_kwargs - ), - ) # else: user OK, so we check MAC now # If we are authenticating with mac, we look for the interfaces and its mac address if port_profile.radius_mode == "COMMON" or port_profile.radius_mode == "STRICT": - # Mac auth - interface = ( - Interface.objects.filter(mac_address=mac_address) - .select_related("machine__user") - .select_related("ipv4") - .first() - ) # If mac is unknown, - if not interface: - room = port.room + if not user_interface: # We try to register mac, if autocapture is enabled # Final decision depend on RADIUSOption set in re2o - if nas_type.autocapture_mac: + if nas_type["autocapture_mac"]: return ( - sw_name, - room, "Unknown mac/interface", - getattr( - RadiusOption.get_cached_value("unknown_machine_vlan"), - "vlan_id", - None, - ), - RadiusOption.get_cached_value("unknown_machine") - != RadiusOption.REJECT, - RadiusOption.get_attributes( - "unknown_machine_attributes", attributes_kwargs - ), + radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, + radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["unknown_machine_attributes"], attributes_kwargs), ) # Otherwise, if autocapture mac is not enabled, else: return ( - sw_name, - "", "Unknown mac/interface", - getattr( - RadiusOption.get_cached_value("unknown_machine_vlan"), - "vlan_id", - None, - ), - RadiusOption.get_cached_value("unknown_machine") - != RadiusOption.REJECT, - RadiusOption.get_attributes( - "unknown_machine_attributes", attributes_kwargs - ), + radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, + radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["unknown_machine_attributes"], attributes_kwargs), ) # Mac/Interface is found, check if related user is contributing and ok # If needed, set ipv4 to it else: - room = port.room - if interface.machine.user.is_ban(): + if user_interface["is_ban"]: return ( - sw_name, - room, "Banned user", - getattr( - RadiusOption.get_cached_value("banned_vlan"), "vlan_id", None - ), - RadiusOption.get_cached_value("banned") != RadiusOption.REJECT, - RadiusOption.get_attributes("banned_attributes", attributes_kwargs), + radius_option["banned_vlan"] and radius_option["banned_vlan"]["vlan_id"] or None, + radius_option["banned"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["banned_attributes"], attributes_kwargs), ) - if not interface.is_active: + if not user_interface["active"]: return ( - sw_name, - room, "Disabled interface / non-contributing member", - getattr( - RadiusOption.get_cached_value("non_member_vlan"), - "vlan_id", - None, - ), - RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, - RadiusOption.get_attributes( - "non_member_attributes", attributes_kwargs - ), + radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, + radius_option["non_member"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["non_member_attributes"], attributes_kwargs), ) # If settings is set to related interface vlan policy based on interface type: - if RadiusOption.get_cached_value("radius_general_policy") == "MACHINE": - DECISION_VLAN = interface.machine_type.ip_type.vlan.vlan_id - if not interface.ipv4: - interface.assign_ipv4() + if radius_option["radius_general_policy"] == "MACHINE": + DECISION_VLAN = user_interface["vlan_id"] + if not user_interface["ipv4"]: + # interface.assign_ipv4() return ( - sw_name, - room, "Ok, assigning new ipv4" + extra_log, DECISION_VLAN, True, @@ -576,8 +500,6 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): ) else: return ( - sw_name, - room, "Interface OK" + extra_log, DECISION_VLAN, True, diff --git a/radius/api/serializers.py b/radius/api/serializers.py index 41959eb9..1ad2e2eb 100644 --- a/radius/api/serializers.py +++ b/radius/api/serializers.py @@ -1,3 +1,24 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2020 Corentin Canebier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + from rest_framework import serializers import machines.models as machines @@ -6,13 +27,15 @@ from api.serializers import NamespacedHMSerializer from rest_framework.serializers import Serializer +class Ipv4Serializer(Serializer): + ipv4 = serializers.CharField() + + class InterfaceSerializer(Serializer): mac_address = serializers.CharField() - ipv4 = serializers.CharField(source="ipv4.ipv4") + ipv4 = Ipv4Serializer() active = serializers.BooleanField(source="is_active") user_pk = serializers.CharField(source="machine.user.pk") - # machine_type_pk = serializers.CharField(source="machine_type.pk") - # switch_stack = serializers.CharField(source="machine.switch.stack") machine_short_name = serializers.CharField(source="machine.short_name") is_ban = serializers.BooleanField(source="machine.user.is_ban") vlan_id = serializers.IntegerField( @@ -96,3 +119,4 @@ class PostAuthResponseSerializer(Serializer): radius_option = RadiusOptionSerializer() EMAIL_STATE_UNVERIFIED = serializers.IntegerField() RADIUS_OPTION_REJECT = serializers.CharField() + USER_STATE_ACTIVE = serializers.CharField() diff --git a/radius/api/urls.py b/radius/api/urls.py index af532a5b..8832771a 100644 --- a/radius/api/urls.py +++ b/radius/api/urls.py @@ -1,5 +1,5 @@ # -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # @@ -21,15 +21,9 @@ from . import views -urls_view = [ - # (r"radius/nas-interface-from-id/(?P.+)$", views.nas_from_id_view), - # (r"radius/nas-from-machine-type/(?P.+)$", views.NasFromMachineTypeView), - # (r"radius/user-from-username/(?P.+)$", views.UserFromUsernameView), - # (r"radius/interface-from-mac-address/(?P.+)$", views.InterfaceFromMacAddressView), -] urls_functional_view = [ - (r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F:\-]{17})$", + (r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", views.authorize, None), - (r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F:\-]{17})$", + (r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", views.post_auth, None), ] diff --git a/radius/api/views.py b/radius/api/views.py index 7c16f916..55f24fbf 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -1,5 +1,5 @@ # -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # @@ -62,7 +62,7 @@ def authorize(request, nas_id, username, mac_address): class PostAuthResponse: - def __init__(self, nas, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT): + def __init__(self, nas, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE): self.nas = nas self.room_users = room_users self.port = port @@ -72,6 +72,7 @@ class PostAuthResponse: self.radius_option = radius_option self.EMAIL_STATE_UNVERIFIED = EMAIL_STATE_UNVERIFIED self.RADIUS_OPTION_REJECT = RADIUS_OPTION_REJECT + self.USER_STATE_ACTIVE = USER_STATE_ACTIVE def can_view(self, user): return [True] @@ -84,29 +85,33 @@ def post_auth(request, nas_id, nas_port, user_mac): Q(domain=Domain.objects.filter(name=nas_id)) | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) ).first() - print(nas_id) nas_type = None if nas_interface: nas_type = Nas.objects.filter( nas_type=nas_interface.machine_type).first() # get switch - switch = Switch.objects.filter(machine_ptr=nas_interface.machine).first() - if hasattr(nas_interface.machine, "switch"): - stack = nas_interface.machine.switch.stack - if stack: - id_stack_member = nas_port.split("-")[1].split("/")[0] - switch = ( - Switch.objects.filter(stack=stack) - .filter(stack_member_id=id_stack_member) - .first() - ) + switch = None + if nas_interface: + switch = Switch.objects.filter( + machine_ptr=nas_interface.machine).first() + if hasattr(nas_interface.machine, "switch"): + stack = nas_interface.machine.switch.stack + if stack: + id_stack_member = nas_port.split("-")[1].split("/")[0] + switch = ( + Switch.objects.filter(stack=stack) + .filter(stack_member_id=id_stack_member) + .first() + ) # get port port_number = nas_port.split(".")[0].split("/")[-1][-2:] port = Port.objects.filter(switch=switch, port=port_number).first() - port_profile = port.get_port_profile + port_profile = None + if port: + port_profile = port.get_port_profile # get user_interface user_interface = ( @@ -117,9 +122,11 @@ def post_auth(request, nas_id, nas_port, user_mac): ) # get room users - room_users = User.objects.filter( - Q(club__room=port.room) | Q(adherent__room=port.room) - ) + room_users = [] + if port: + room_users = User.objects.filter( + Q(club__room=port.room) | Q(adherent__room=port.room) + ) # get radius options radius_option = RadiusOption.objects.first() @@ -127,7 +134,8 @@ def post_auth(request, nas_id, nas_port, user_mac): EMAIL_STATE_UNVERIFIED = User.EMAIL_STATE_UNVERIFIED RADIUS_OPTION_REJECT = RadiusOption.REJECT + USER_STATE_ACTIVE = User.STATE_ACTIVE serialized = serializers.PostAuthResponseSerializer( - PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT)) + PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE)) return Response(data=serialized.data) From a323bf7d68153c2dcba7681a958140ed8e50dcff Mon Sep 17 00:00:00 2001 From: chapeau Date: Sat, 28 Nov 2020 16:24:36 +0100 Subject: [PATCH 04/22] =?UTF-8?q?premi=C3=A8re=20passe=20de=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freeradius_utils/auth.py | 14 +++++++------- radius/api/serializers.py | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 4c507f62..b5e1cddb 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -195,14 +195,14 @@ def post_auth(data): sw_name = switch["name"] or "?" room = "Unknown port" if port: - room = port.room or "Unknown room" + room = port["room"] or "Unknown room" out = decide_vlan_switch(data_from_api, mac, nas_port) reason, vlan_id, decision, attributes = out if decision: log_message = "(wired) %s -> %s [%s%s]" % ( - sw_name + ":" + port + "/" + str(room), + sw_name + ":" + nas_port + "/" + str(room), mac, vlan_id, (reason and ": " + reason), @@ -221,8 +221,8 @@ def post_auth(data): (), ) else: - log_message = "(fil) %s -> %s [Reject %s]" % ( - sw_name + ":" + port + "/" + str(room), + log_message = "(wired) %s -> %s [Reject %s]" % ( + sw_name + ":" + nas_port + "/" + str(room), mac, (reason and ": " + reason), ) @@ -272,7 +272,7 @@ def check_user_machine_and_register(nas_type, user, user_interface): def set_radius_attributes_values(attributes, values): return ( - (str(attribute.attribute), str(attribute.value % values)) + (str(attribute["attribute"]), str(attribute["value"] % values)) for attribute in attributes ) @@ -325,7 +325,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): attributes_kwargs = { "client_mac": str(user_mac), "switch_port": str(nas_port.split(".")[0].split("/")[-1][-2:]), - "switch_ip": str(switch.ipv4) + "switch_ip": str(switch["ipv4"]) } # Get port from switch and port number @@ -445,7 +445,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): # else: user OK, so we check MAC now # If we are authenticating with mac, we look for the interfaces and its mac address - if port_profile.radius_mode == "COMMON" or port_profile.radius_mode == "STRICT": + if port_profile["radius_mode"] == "COMMON" or port_profile["radius_mode"] == "STRICT": # If mac is unknown, if not user_interface: # We try to register mac, if autocapture is enabled diff --git a/radius/api/serializers.py b/radius/api/serializers.py index 1ad2e2eb..33b4127a 100644 --- a/radius/api/serializers.py +++ b/radius/api/serializers.py @@ -70,6 +70,7 @@ class VlanSerializer(Serializer): class PortProfileSerializer(Serializer): vlan_untagged = VlanSerializer() radius_type = serializers.CharField() + radius_mode = serializers.CharField() class SwitchSerializer(Serializer): From d1a1d6613da5e424a8b2b538ee0aa5f841cc5887 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:29:49 +0200 Subject: [PATCH 05/22] merge --- freeradius_utils/auth.py | 71 +++++++++++++++++++++++----------------- radius/api/urls.py | 4 +++ radius/api/views.py | 51 +++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 32 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index b5e1cddb..ce947bea 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -1,5 +1,5 @@ # -*- mode: python; coding: utf-8 -*- -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # @@ -71,7 +71,7 @@ class RadiusdHandler(logging.Handler): rad_sig = radiusd.L_INFO else: rad_sig = radiusd.L_DBG - radiusd.radlog(rad_sig, record.msg.encode("utf-8")) + radiusd.radlog(rad_sig, str(record.msg)) # Init for logging @@ -126,18 +126,6 @@ def instantiate(*_): """Usefull for instantiate ldap connexions otherwise, do nothing""" logger.info("Instantiation") - path = (os.path.dirname(os.path.abspath(__file__))) - - config = ConfigParser() - config.read(path+'/config.ini') - - api_hostname = config.get('Re2o', 'hostname') - api_password = config.get('Re2o', 'password') - api_username = config.get('Re2o', 'username') - - global api_client - api_client = Re2oAPIClient( - api_hostname, api_username, api_password, use_tls=True) @radius_event @@ -234,7 +222,7 @@ def post_auth(data): return radiusd.RLM_MODULE_OK -def check_user_machine_and_register(nas_type, user, user_interface): +def check_user_machine_and_register(nas_type, user, user_interface, nas_id, username, mac_address): """Check if username and mac are registered. Register it if unknown. Return the user ntlm password if everything is ok. Used for 802.1X auth""" @@ -252,17 +240,28 @@ def check_user_machine_and_register(nas_type, user, user_interface): elif not user_interface["active"]: return (False, "Interface/Machine disabled", "") elif not user_interface["ipv4"]: - # interface.assign_ipv4() - return (True, "Ok, new ipv4 assignement...", user.get("pwd_ntlm", "")) + try: + api_client.view( + "radius/assign_ip/{0}".format( + urllib.parse.quote(mac_address or "None", safe="") + )) + return (True, "Ok, new ipv4 assignement...", user.get("pwd_ntlm", "")) + except HTTPError as err: + return (False, "Error during ip assignement %s" % err.response.text, "") else: return (True, "Access ok", user.get("pwd_ntlm", "")) elif nas_type: if nas_type["autocapture_mac"]: - # result, reason = user.autoregister_machine(mac_address, nas_type) - # if result: - # return (True, "Access Ok, Registering mac...", user.pwd_ntlm) - # else: - # return (False, "Error during mac register %s" % reason, "") + try: + api_client.view( + "radius/autoregister/{0}/{1}/{2}".format( + urllib.parse.quote(nas_id or "None", safe=""), + urllib.parse.quote(username or "None", safe=""), + urllib.parse.quote(mac_address or "None", safe="") + )) + return (True, "Access Ok, Registering mac...", user["pwd_ntlm"]) + except HTTPError as err: + return (False, "Error during mac register %s" % err.response.text, "") return (False, "L'auto capture est désactivée", "") else: return (False, "Unknown interface/machine", "") @@ -423,7 +422,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): for user in room_users: if not user["is_ban"] and user["state"] == USER_STATE_ACTIVE: all_user_ban = False - elif user["email_state"] != EMAIL_STATE_UNVERIFIED and (user["is_connected"] or user["is_whitelisted"]): + if user["email_state"] != EMAIL_STATE_UNVERIFIED and (user["is_connected"] or user["is_whitelisted"]): at_least_one_active_user = True if all_user_ban: @@ -491,13 +490,25 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): if radius_option["radius_general_policy"] == "MACHINE": DECISION_VLAN = user_interface["vlan_id"] if not user_interface["ipv4"]: - # interface.assign_ipv4() - return ( - "Ok, assigning new ipv4" + extra_log, - DECISION_VLAN, - True, - attributes, - ) + try: + api_client.view( + "radius/assign_ip/{0}".format( + urllib.parse.quote(user_mac or "None", safe="") + )) + return ( + "Ok, assigning new ipv4" + extra_log, + DECISION_VLAN, + True, + attributes, + ) + except HTTPError as err: + return ( + "Error during ip assignement %s" % err.response.text + extra_log, + DECISION_VLAN, + True, + attributes, + ) + else: return ( "Interface OK" + extra_log, diff --git a/radius/api/urls.py b/radius/api/urls.py index 8832771a..528e015b 100644 --- a/radius/api/urls.py +++ b/radius/api/urls.py @@ -26,4 +26,8 @@ urls_functional_view = [ views.authorize, None), (r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", views.post_auth, None), + (r"radius/autoregister/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + views.autoregister_machine, None), + (r"radius/assign_ip/(?P[0-9a-fA-F\:\-]{17})$", + views.assign_ip, None), ] diff --git a/radius/api/views.py b/radius/api/views.py index 55f24fbf..72576348 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -22,6 +22,8 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from django.db.models import Q +from django.http import HttpResponse +from django.forms import ValidationError from . import serializers from machines.models import Domain, IpList, Interface, Nas @@ -42,6 +44,16 @@ class AuthorizeResponse: @api_view(['GET']) def authorize(request, nas_id, username, mac_address): + """Return objects the radius need for the Authorize step + + Parameters: + nas_id (string): NAS name or ipv4 + username (string): username of the user who is trying to connect + mac_address (string): mac address of the device which is trying to connect + + Return: + AuthorizeResponse: contains all the informations + """ nas_interface = Interface.objects.filter( Q(domain=Domain.objects.filter(name=nas_id)) @@ -106,8 +118,10 @@ def post_auth(request, nas_id, nas_port, user_mac): ) # get port - port_number = nas_port.split(".")[0].split("/")[-1][-2:] - port = Port.objects.filter(switch=switch, port=port_number).first() + port = None + if nas_port and nas_port != "None": + port_number = nas_port.split(".")[0].split("/")[-1][-2:] + port = Port.objects.filter(switch=switch, port=port_number).first() port_profile = None if port: @@ -139,3 +153,36 @@ def post_auth(request, nas_id, nas_port, user_mac): PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE)) return Response(data=serialized.data) + + +@api_view(['GET']) +def autoregister_machine(request, nas_id, username, mac_address): + nas_interface = Interface.objects.filter( + Q(domain=Domain.objects.filter(name=nas_id)) + | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + ).first() + nas_type = None + if nas_interface: + nas_type = Nas.objects.filter( + nas_type=nas_interface.machine_type).first() + + user = User.objects.filter(pseudo__iexact=username).first() + + result, reason = user.autoregister_machine(mac_address, nas_type) + if result: + return Response(data=reason) + return Response(reason, status=400) + + +@api_view(['GET']) +def assign_ip(request, mac_address): + interface = ( + Interface.objects.filter(mac_address=mac_address) + .first() + ) + + try: + interface.assign_ipv4() + return Response() + except ValidationError as err: + return Response(err.message, status=400) From ce862c2853ad25d1a7d4caf79f0414e6c0e085d0 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:30:52 +0200 Subject: [PATCH 06/22] merge --- freeradius_utils/auth.py | 222 +++++++++++++++++++++----------------- radius/api/serializers.py | 6 ++ radius/api/urls.py | 3 +- radius/api/views.py | 68 ++++++++++-- 4 files changed, 189 insertions(+), 110 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index ce947bea..328d2cd0 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -61,10 +61,10 @@ from topologie.models import Port, Switch from users.models import User class RadiusdHandler(logging.Handler): - """Handler de logs pour freeradius""" + """Logs handler for freeradius""" def emit(self, record): - """Process un message de log, en convertissant les niveaux""" + """Log message processing, level are converted""" if record.levelno >= logging.WARN: rad_sig = radiusd.L_ERR elif record.levelno >= logging.INFO: @@ -84,17 +84,15 @@ logger.addHandler(handler) def radius_event(fun): - """Décorateur pour les fonctions d'interfaces avec radius. - Une telle fonction prend un uniquement argument, qui est une liste de - tuples (clé, valeur) et renvoie un triplet dont les composantes sont : - * le code de retour (voir radiusd.RLM_MODULE_* ) - * un tuple de couples (clé, valeur) pour les valeurs de réponse (accès ok - et autres trucs du genre) - * un tuple de couples (clé, valeur) pour les valeurs internes à mettre à - jour (mot de passe par exemple) + """Decorator for freeradius fonction with radius. + This function take a unique argument which is a list of tuples (key, value) + and return a tuple of 3 values which are: + * return code (see radiusd.RLM_MODULE_* ) + * a tuple of 2 elements for response value (access ok , etc) + * a tuple of 2 elements for internal value to update (password for example) - On se contente avec ce décorateur (pour l'instant) de convertir la liste de - tuples en entrée en un dictionnaire.""" + Here, we convert the list of tuples into a dictionnary. + """ def new_f(auth_data): """ The function transforming the tuples as dict """ @@ -123,10 +121,23 @@ def radius_event(fun): @radius_event def instantiate(*_): - """Usefull for instantiate ldap connexions otherwise, - do nothing""" + """Instantiate api connection + """ logger.info("Instantiation") + path = (os.path.dirname(os.path.abspath(__file__))) + + config = ConfigParser() + config.read(path+'/config.ini') + + api_hostname = config.get('Re2o', 'hostname') + api_password = config.get('Re2o', 'password') + api_username = config.get('Re2o', 'username') + + global api_client + api_client = Re2oAPIClient( + api_hostname, api_username, api_password, use_tls=True) + @radius_event def authorize(data): @@ -178,8 +189,9 @@ def post_auth(data): logger.info("Proxified request, nas unknown") return radiusd.RLM_MODULE_OK - # If it is a switch + # If the request is from a switch (wired connection) if switch: + # For logging sw_name = switch["name"] or "?" room = "Unknown port" if port: @@ -197,7 +209,7 @@ def post_auth(data): ) logger.info(log_message) - # Wired connexion + # Apply vlan from decide_vlan_switch return ( radiusd.RLM_MODULE_UPDATED, ( @@ -218,6 +230,7 @@ def post_auth(data): return (radiusd.RLM_MODULE_REJECT, tuple(attributes), ()) + # Else it is from wifi else: return radiusd.RLM_MODULE_OK @@ -225,11 +238,16 @@ def post_auth(data): def check_user_machine_and_register(nas_type, user, user_interface, nas_id, username, mac_address): """Check if username and mac are registered. Register it if unknown. Return the user ntlm password if everything is ok. - Used for 802.1X auth""" + Used for 802.1X auth + """ + if not user: + # No username provided return (False, "User unknown", "") + if not user["access"]: return (False, "Invalid connexion (non-contributing user)", "") + if user_interface: if user_interface["user_pk"] != user["pk"]: return ( @@ -237,9 +255,12 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user "Mac address registered on another user account", "", ) + elif not user_interface["active"]: return (False, "Interface/Machine disabled", "") + elif not user_interface["ipv4"]: + # Try to autoassign ip try: api_client.view( "radius/assign_ip/{0}".format( @@ -250,7 +271,9 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user return (False, "Error during ip assignement %s" % err.response.text, "") else: return (True, "Access ok", user.get("pwd_ntlm", "")) + elif nas_type: + # The interface is not yet registred, try to autoregister if enabled if nas_type["autocapture_mac"]: try: api_client.view( @@ -262,7 +285,7 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user return (True, "Access Ok, Registering mac...", user["pwd_ntlm"]) except HTTPError as err: return (False, "Error during mac register %s" % err.response.text, "") - return (False, "L'auto capture est désactivée", "") + return (False, "Autoregistering is disabled", "") else: return (False, "Unknown interface/machine", "") else: @@ -270,6 +293,7 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user def set_radius_attributes_values(attributes, values): + """Set values of parameters in radius attributes""" return ( (str(attribute["attribute"]), str(attribute["value"] % values)) for attribute in attributes @@ -278,12 +302,14 @@ def set_radius_attributes_values(attributes, values): def decide_vlan_switch(data_from_api, user_mac, nas_port): """Function for selecting vlan for a switch with wired mac auth radius. - Several modes are available : + Two modes exist : in strict mode, a registered user cannot connect with + their machines in a non-registered user room + Sequentially : - all modes: - - unknown NAS : VLAN_OK, - - unknown port : Decision set in Re2o RadiusOption - - No radius on this port : VLAN_OK - - force : returns vlan provided by the database + - unknown NAS : VLAN_OK, + - unknown port : Decision set in Re2o RadiusOption + - No radius on this port : VLAN_OK + - force : replace VLAN_OK with vlan provided by the database - mode strict: - no room : Decision set in Re2o RadiusOption, - no user in this room : Reject, @@ -293,23 +319,22 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): - mode common : - mac-address already registered: - related user non contributor / interface disabled: - Decision set in Re2o RadiusOption + Decision set in Re2o RadiusOption - related user is banned: - Decision set in Re2o RadiusOption + Decision set in Re2o RadiusOption - user contributing : VLAN_OK (can assign ipv4 if needed) - unknown interface : - register mac disabled : Decision set in Re2o RadiusOption - - register mac enabled : redirect to webauth + - register mac enabled : redirect to webauth (not implemented) Returns: tuple with : - - Switch name (str) - - Room (str) - Reason of the decision (str) - vlan_id (int) - decision (bool) - - Other Attributs (attribut:str, operator:str, value:str) + - Other Attributs (attribut:str, value:str) """ + # Get values from api nas_type = data_from_api["nas"] room_users = data_from_api["room_users"] port = data_from_api["port"] @@ -321,13 +346,14 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): RADIUS_OPTION_REJECT = data_from_api["RADIUS_OPTION_REJECT"] USER_STATE_ACTIVE = data_from_api["USER_STATE_ACTIVE"] + # Values which can be used as parameters in radius attributes attributes_kwargs = { "client_mac": str(user_mac), + # magic split "switch_port": str(nas_port.split(".")[0].split("/")[-1][-2:]), "switch_ip": str(switch["ipv4"]) } - # Get port from switch and port number extra_log = "" # If NAS is unknown, go to default vlan if not nas_machine: @@ -346,8 +372,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): attributes_kwargs["switch_ip"] = str(switch.ipv4) port = Port.objects.filter(switch=switch, port=port_number).first() - # If the port is unknwon, go to default vlan - # We don't have enought information to make a better decision + # If the port is unknown, do as in RadiusOption if not port or not port_profile: return ( "Unknown port", @@ -395,7 +420,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): # Otherwise, we are in mac radius. # If strict mode is enabled, we check every user related with this port. If - # one user or more is not enabled, we reject to prevent from sharing or + # all users and clubs are disabled, we reject to prevent from sharing or # spoofing mac. if port_profile["radius_mode"] == "STRICT": if not port["room"]: @@ -443,76 +468,75 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): ) # else: user OK, so we check MAC now - # If we are authenticating with mac, we look for the interfaces and its mac address - if port_profile["radius_mode"] == "COMMON" or port_profile["radius_mode"] == "STRICT": - # If mac is unknown, - if not user_interface: - # We try to register mac, if autocapture is enabled - # Final decision depend on RADIUSOption set in re2o - if nas_type["autocapture_mac"]: - return ( - "Unknown mac/interface", - radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, - radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["unknown_machine_attributes"], attributes_kwargs), - ) - # Otherwise, if autocapture mac is not enabled, - else: - return ( - "Unknown mac/interface", - radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, - radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["unknown_machine_attributes"], attributes_kwargs), - ) - - # Mac/Interface is found, check if related user is contributing and ok - # If needed, set ipv4 to it + # If mac is unknown, + if not user_interface: + # We try to register mac, if autocapture is enabled + # Final decision depend on RADIUSOption set in re2o + # Something is not implemented here... + if nas_type["autocapture_mac"]: + return ( + "Unknown mac/interface", + radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, + radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["unknown_machine_attributes"], attributes_kwargs), + ) + # Otherwise, if autocapture mac is not enabled, else: - if user_interface["is_ban"]: - return ( - "Banned user", - radius_option["banned_vlan"] and radius_option["banned_vlan"]["vlan_id"] or None, - radius_option["banned"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["banned_attributes"], attributes_kwargs), - ) - if not user_interface["active"]: - return ( - "Disabled interface / non-contributing member", - radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, - radius_option["non_member"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["non_member_attributes"], attributes_kwargs), - ) - # If settings is set to related interface vlan policy based on interface type: - if radius_option["radius_general_policy"] == "MACHINE": - DECISION_VLAN = user_interface["vlan_id"] - if not user_interface["ipv4"]: - try: - api_client.view( - "radius/assign_ip/{0}".format( - urllib.parse.quote(user_mac or "None", safe="") - )) - return ( - "Ok, assigning new ipv4" + extra_log, - DECISION_VLAN, - True, - attributes, - ) - except HTTPError as err: - return ( - "Error during ip assignement %s" % err.response.text + extra_log, - DECISION_VLAN, - True, - attributes, - ) + return ( + "Unknown mac/interface", + radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, + radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["unknown_machine_attributes"], attributes_kwargs), + ) - else: + # Mac/Interface is found, check if related user is contributing and ok + # If needed, set ipv4 to it + else: + if user_interface["is_ban"]: + return ( + "Banned user", + radius_option["banned_vlan"] and radius_option["banned_vlan"]["vlan_id"] or None, + radius_option["banned"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["banned_attributes"], attributes_kwargs), + ) + if not user_interface["active"]: + return ( + "Disabled interface / non-contributing member", + radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, + radius_option["non_member"] != RADIUS_OPTION_REJECT, + set_radius_attributes_values( + radius_option["non_member_attributes"], attributes_kwargs), + ) + # If settings is set to related interface vlan policy based on interface type: + if radius_option["radius_general_policy"] == "MACHINE": + DECISION_VLAN = user_interface["vlan_id"] + if not user_interface["ipv4"]: + try: + api_client.view( + "radius/assign_ip/{0}".format( + urllib.parse.quote(user_mac or "None", safe="") + )) return ( - "Interface OK" + extra_log, + "Ok, assigning new ipv4" + extra_log, DECISION_VLAN, True, attributes, ) + except HTTPError as err: + return ( + "Error during ip assignement %s" % err.response.text + extra_log, + DECISION_VLAN, + True, + attributes, + ) + + else: + return ( + "Interface OK" + extra_log, + DECISION_VLAN, + True, + attributes, + ) diff --git a/radius/api/serializers.py b/radius/api/serializers.py index 33b4127a..867f7a54 100644 --- a/radius/api/serializers.py +++ b/radius/api/serializers.py @@ -105,12 +105,18 @@ class RadiusOptionSerializer(Serializer): class AuthorizeResponseSerializer(Serializer): + """Serializer for AuthorizeResponse objects + See views.py for the declaration of AuthorizeResponse + """ nas = NasSerializer(read_only=True) user = UserSerializer(read_only=True) user_interface = InterfaceSerializer(read_only=True) class PostAuthResponseSerializer(Serializer): + """Serializer for PostAuthResponse objects + See views.py for the declaration of PostAuthResponse + """ nas = NasSerializer(read_only=True) room_users = UserSerializer(many=True) port = PortSerializer() diff --git a/radius/api/urls.py b/radius/api/urls.py index 528e015b..d24b89eb 100644 --- a/radius/api/urls.py +++ b/radius/api/urls.py @@ -28,6 +28,5 @@ urls_functional_view = [ views.post_auth, None), (r"radius/autoregister/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", views.autoregister_machine, None), - (r"radius/assign_ip/(?P[0-9a-fA-F\:\-]{17})$", - views.assign_ip, None), + (r"radius/assign_ip/(?P[0-9a-fA-F\:\-]{17})$", views.assign_ip, None), ] diff --git a/radius/api/views.py b/radius/api/views.py index 72576348..7ace6013 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -33,18 +33,23 @@ from topologie.models import Port, Switch class AuthorizeResponse: + """Contains objects the radius needs for the Authorize step + """ + def __init__(self, nas, user, user_interface): self.nas = nas self.user = user self.user_interface = user_interface def can_view(self, user): + """Temp method to bypass ACL + """ return [True] @api_view(['GET']) def authorize(request, nas_id, username, mac_address): - """Return objects the radius need for the Authorize step + """Return objects the radius needs for the Authorize step Parameters: nas_id (string): NAS name or ipv4 @@ -52,9 +57,10 @@ def authorize(request, nas_id, username, mac_address): mac_address (string): mac address of the device which is trying to connect Return: - AuthorizeResponse: contains all the informations + AuthorizeResponse: contains all required informations """ + # get the Nas object which made the request (if exists) nas_interface = Interface.objects.filter( Q(domain=Domain.objects.filter(name=nas_id)) | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) @@ -64,7 +70,11 @@ def authorize(request, nas_id, username, mac_address): nas_type = Nas.objects.filter( nas_type=nas_interface.machine_type).first() + # get the User corresponding to the username in the URL + # If no username was provided (wired connection), username="None" user = User.objects.filter(pseudo__iexact=username).first() + + # get the interface which is trying to connect (if already created) user_interface = Interface.objects.filter(mac_address=mac_address).first() serialized = serializers.AuthorizeResponseSerializer( @@ -74,6 +84,9 @@ def authorize(request, nas_id, username, mac_address): class PostAuthResponse: + """Contains objects the radius needs for the Post-Auth step + """ + def __init__(self, nas, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE): self.nas = nas self.room_users = room_users @@ -92,7 +105,18 @@ class PostAuthResponse: @api_view(['GET']) def post_auth(request, nas_id, nas_port, user_mac): - # get nas_type + """Return objects the radius needs for the Post-Auth step + + Parameters: + nas_id (string): NAS name or ipv4 + nas_port (string): NAS port from wich the request came. Work with Cisco, HP and Juniper convention + user_mac (string): mac address of the device which is trying to connect + + Return: + PostAuthResponse: contains all required informations + """ + + # get the Nas object which made the request (if exists) nas_interface = Interface.objects.prefetch_related("machine__switch__stack").filter( Q(domain=Domain.objects.filter(name=nas_id)) | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) @@ -102,14 +126,17 @@ def post_auth(request, nas_id, nas_port, user_mac): nas_type = Nas.objects.filter( nas_type=nas_interface.machine_type).first() - # get switch + # get the switch (if wired connection) switch = None if nas_interface: switch = Switch.objects.filter( machine_ptr=nas_interface.machine).first() + + # If the switch is part of a stack, get the correct object if hasattr(nas_interface.machine, "switch"): stack = nas_interface.machine.switch.stack if stack: + # magic split id_stack_member = nas_port.split("-")[1].split("/")[0] switch = ( Switch.objects.filter(stack=stack) @@ -117,9 +144,10 @@ def post_auth(request, nas_id, nas_port, user_mac): .first() ) - # get port + # get the switch port port = None if nas_port and nas_port != "None": + # magic split port_number = nas_port.split(".")[0].split("/")[-1][-2:] port = Port.objects.filter(switch=switch, port=port_number).first() @@ -127,7 +155,7 @@ def post_auth(request, nas_id, nas_port, user_mac): if port: port_profile = port.get_port_profile - # get user_interface + # get the interface which is trying to connect (if already created) user_interface = ( Interface.objects.filter(mac_address=user_mac) .select_related("machine__user") @@ -135,20 +163,22 @@ def post_auth(request, nas_id, nas_port, user_mac): .first() ) - # get room users + # get all users and clubs of the room room_users = [] if port: room_users = User.objects.filter( Q(club__room=port.room) | Q(adherent__room=port.room) ) - # get radius options + # get all radius options radius_option = RadiusOption.objects.first() print(radius_option) + # get a few class constants the radius will need EMAIL_STATE_UNVERIFIED = User.EMAIL_STATE_UNVERIFIED RADIUS_OPTION_REJECT = RadiusOption.REJECT USER_STATE_ACTIVE = User.STATE_ACTIVE + serialized = serializers.PostAuthResponseSerializer( PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE)) @@ -157,6 +187,17 @@ def post_auth(request, nas_id, nas_port, user_mac): @api_view(['GET']) def autoregister_machine(request, nas_id, username, mac_address): + """Autoregister machine in the Authorize step of the radius + + Parameters: + nas_id (string): NAS name or ipv4 + username (string): username of the user who is trying to connect + mac_address (string): mac address of the device which is trying to connect + + Return: + 200 if autoregistering worked + 400 if it failed, and the reason why + """ nas_interface = Interface.objects.filter( Q(domain=Domain.objects.filter(name=nas_id)) | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) @@ -170,12 +211,21 @@ def autoregister_machine(request, nas_id, username, mac_address): result, reason = user.autoregister_machine(mac_address, nas_type) if result: - return Response(data=reason) + return Response(reason) return Response(reason, status=400) @api_view(['GET']) def assign_ip(request, mac_address): + """Autoassign ip in the Authorize and Post-Auth steps of the Radius + + Parameters: + mac_address (string): mac address of the device which is trying to connect + + Return: + 200 if it worked + 400 if it failed, and the reason why + """ interface = ( Interface.objects.filter(mac_address=mac_address) .first() From f689d6640ac82e1413502a4b37deea1dc778659f Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 29 Nov 2020 18:08:20 +0100 Subject: [PATCH 07/22] add brand news ACL --- radius/api/views.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/radius/api/views.py b/radius/api/views.py index 7ace6013..2f10f916 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -24,12 +24,14 @@ from rest_framework.response import Response from django.db.models import Q from django.http import HttpResponse from django.forms import ValidationError +from django.contrib.auth.decorators import login_required from . import serializers -from machines.models import Domain, IpList, Interface, Nas +from machines.models import Domain, IpList, Interface, Nas, Machine from users.models import User from preferences.models import RadiusOption from topologie.models import Port, Switch +from re2o.acl import can_view_all_api, can_edit_all_api, can_create_api class AuthorizeResponse: @@ -48,6 +50,8 @@ class AuthorizeResponse: @api_view(['GET']) +@login_required +@can_view_all_api(Interface, Domain, IpList, Nas, User) def authorize(request, nas_id, username, mac_address): """Return objects the radius needs for the Authorize step @@ -104,6 +108,8 @@ class PostAuthResponse: @api_view(['GET']) +@login_required +@can_view_all_api(Interface, Domain, IpList, Nas, Switch, Port, User) def post_auth(request, nas_id, nas_port, user_mac): """Return objects the radius needs for the Post-Auth step @@ -186,6 +192,9 @@ def post_auth(request, nas_id, nas_port, user_mac): @api_view(['GET']) +@login_required +@can_view_all_api(Interface, Domain, IpList, Nas, User) +@can_edit_all_api(User, Domain, Machine, Interface) def autoregister_machine(request, nas_id, username, mac_address): """Autoregister machine in the Authorize step of the radius @@ -216,6 +225,8 @@ def autoregister_machine(request, nas_id, username, mac_address): @api_view(['GET']) +@can_view_all_api(Interface) +@can_edit_all_api(Interface) def assign_ip(request, mac_address): """Autoassign ip in the Authorize and Post-Auth steps of the Radius From 1bc5b5143983a12a69ea84de09e542a2ba75c5bd Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 29 Nov 2020 22:37:42 +0100 Subject: [PATCH 08/22] a few small fixes --- freeradius_utils/auth.py | 6 ++---- re2o/acl.py | 1 + re2o/mixins.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 328d2cd0..7c32f05c 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -193,9 +193,7 @@ def post_auth(data): if switch: # For logging sw_name = switch["name"] or "?" - room = "Unknown port" - if port: - room = port["room"] or "Unknown room" + room = port["room"] or "Unknown room" if port else "Unknown port" out = decide_vlan_switch(data_from_api, mac, nas_port) reason, vlan_id, decision, attributes = out @@ -388,7 +386,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): # If a vlan is precised in port config, we use it if port_profile["vlan_untagged"]: DECISION_VLAN = int(port_profile["vlan_untagged"]["vlan_id"]) - extra_log = "Force sur vlan " + str(DECISION_VLAN) + extra_log = "Force sur vlan %s" % str(DECISION_VLAN) attributes = () else: DECISION_VLAN = radius_option["vlan_decision_ok"]["vlan_id"] diff --git a/re2o/acl.py b/re2o/acl.py index d1d2c3a9..72633256 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -6,6 +6,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Lara Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2020 Corentin Canebier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/re2o/mixins.py b/re2o/mixins.py index ddc827b6..e0ce3310 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -5,6 +5,7 @@ # # Copyright © 2018 Gabriel Détraz # Copyright © 2017 Charlie Jacomme +# Copyright © 2020 Corentin Canebier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From 425d47c52d3e8be30e2f7018a93bf614921f5f38 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:31:27 +0200 Subject: [PATCH 09/22] merge --- freeradius_utils/auth.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 7c32f05c..8ad2ca53 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -35,6 +35,13 @@ https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_pyth Inspired by Daniel Stan in Crans """ +from configparser import ConfigParser + +from re2oapi import Re2oAPIClient + +import sys +from pathlib import Path +import subprocess import logging import os import sys @@ -125,10 +132,10 @@ def instantiate(*_): """ logger.info("Instantiation") - path = (os.path.dirname(os.path.abspath(__file__))) + path = Path(__file__).resolve(strict=True).parent config = ConfigParser() - config.read(path+'/config.ini') + config.read(path / 'config.ini') api_hostname = config.get('Re2o', 'hostname') api_password = config.get('Re2o', 'password') From 989410509db74be56aec4790fe016b2f1f7b0905 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:31:48 +0200 Subject: [PATCH 10/22] merge --- freeradius_utils/auth.py | 72 ++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 8ad2ca53..c5a6b55f 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -141,9 +141,16 @@ def instantiate(*_): api_password = config.get('Re2o', 'password') api_username = config.get('Re2o', 'username') + def get_api_client(): + """Gets a Re2o, or tries to initialize one""" + if get_api_client.client is None: + get_api_client.client = Re2oAPIClient( + api_hostname, api_username, api_password, use_tls=True) + return get_api_client.client + get_api_client.client = None + global api_client - api_client = Re2oAPIClient( - api_hostname, api_username, api_password, use_tls=True) + api_client = get_api_client @radius_event @@ -154,19 +161,26 @@ def authorize(data): - It the nas is known AND nas auth is enabled with mac address, returns accept here""" # For proxified request, split - nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) - nas_instance = find_nas_from_request(nas) - # For none proxified requests - nas_type = None - if nas_instance: - nas_type = Nas.objects.filter(nas_type=nas_instance.machine_type).first() - if not nas_type or nas_type.port_access_mode == "802.1X": - user = data.get("User-Name", "") - user = user.split("@", 1)[0] - mac = data.get("Calling-Station-Id", "") - result, log, password = check_user_machine_and_register(nas_type, user, mac) - logger.info(str(log)) - logger.info(str(user)) + username = username.split("@", 1)[0] + mac = data.get("Calling-Station-Id", "") + + # Get all required objects from API + data_from_api = api_client().view( + "radius/authorize/{0}/{1}/{2}".format( + urllib.parse.quote(nas or "None", safe=""), + urllib.parse.quote(username or "None", safe=""), + urllib.parse.quote(mac or "None", safe="") + )) + + nas_type = data_from_api["nas"] + user = data_from_api["user"] + user_interface = data_from_api["user_interface"] + + if not nas_type or nas_type and nas_type["port_access_mode"] == "802.1X": + result, log, password = check_user_machine_and_register( + nas_type, user, user_interface, nas, username, mac) + logger.info(log.encode("utf-8")) + logger.info(username.encode("utf-8")) if not result: return radiusd.RLM_MODULE_REJECT @@ -186,12 +200,22 @@ def post_auth(data): """Function called after the user is authenticated""" nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) - nas_instance = find_nas_from_request(nas) - # All non proxified requests - if not nas_instance: - logger.info("Proxified request, nas unknown") - return radiusd.RLM_MODULE_OK - nas_type = Nas.objects.filter(nas_type=nas_instance.machine_type).first() + nas_port = data.get("NAS-Port-Id", data.get("NAS-Port", None)) + mac = data.get("Calling-Station-Id", None) + + # Get all required objects from API + data_from_api = api_client().view( + "radius/post_auth/{0}/{1}/{2}".format( + urllib.parse.quote(nas or "None", safe=""), + urllib.parse.quote(nas_port or "None", safe=""), + urllib.parse.quote(mac or "None", safe="") + )) + + nas_type = data_from_api["nas"] + port = data_from_api["port"] + switch = data_from_api["switch"] + + # If proxified request if not nas_type: logger.info("Proxified request, nas unknown") return radiusd.RLM_MODULE_OK @@ -267,7 +291,7 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user elif not user_interface["ipv4"]: # Try to autoassign ip try: - api_client.view( + api_client().view( "radius/assign_ip/{0}".format( urllib.parse.quote(mac_address or "None", safe="") )) @@ -281,7 +305,7 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user # The interface is not yet registred, try to autoregister if enabled if nas_type["autocapture_mac"]: try: - api_client.view( + api_client().view( "radius/autoregister/{0}/{1}/{2}".format( urllib.parse.quote(nas_id or "None", safe=""), urllib.parse.quote(username or "None", safe=""), @@ -520,7 +544,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): DECISION_VLAN = user_interface["vlan_id"] if not user_interface["ipv4"]: try: - api_client.view( + api_client().view( "radius/assign_ip/{0}".format( urllib.parse.quote(user_mac or "None", safe="") )) From f70b97677f5444df4cd8b6f746f04ffd3f8f4d61 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:33:56 +0200 Subject: [PATCH 11/22] separate radius from re2o repo --- freeradius_utils/auth.py | 586 ++++++++++++++++++++------------------- 1 file changed, 301 insertions(+), 285 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index c5a6b55f..f4201f44 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -7,7 +7,6 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Lara Kermarec # Copyright © 2017 Augustin Lemesle -# Copyright © 2020 Corentin Canebier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -35,19 +34,12 @@ https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_pyth Inspired by Daniel Stan in Crans """ -from configparser import ConfigParser - -from re2oapi import Re2oAPIClient - -import sys -from pathlib import Path -import subprocess -import logging import os import sys +import logging import traceback - import radiusd # Magic module freeradius (radiusd.py is dummy) + from django.core.wsgi import get_wsgi_application from django.db.models import Q @@ -62,11 +54,14 @@ os.chdir(proj_path) # This is so models get loaded. application = get_wsgi_application() -from machines.models import Domain, Interface, IpList, Nas -from preferences.models import RadiusOption +from machines.models import Interface, IpList, Nas, Domain from topologie.models import Port, Switch from users.models import User +from preferences.models import RadiusOption + + +# Logging class RadiusdHandler(logging.Handler): """Logs handler for freeradius""" @@ -81,7 +76,7 @@ class RadiusdHandler(logging.Handler): radiusd.radlog(rad_sig, str(record.msg)) -# Init for logging +# Init for logging logger = logging.getLogger("auth.py") logger.setLevel(logging.DEBUG) formatter = logging.Formatter("%(name)s: [%(levelname)s] %(message)s") @@ -112,15 +107,12 @@ def radius_event(fun): # Ex: Calling-Station-Id: "une_adresse_mac" data[key] = value.replace('"', "") try: - # TODO s'assurer ici que les tuples renvoy s sont bien des - # (str,str) : rlm_python ne dig re PAS les unicodes return fun(data) except Exception as err: exc_type, exc_instance, exc_traceback = sys.exc_info() formatted_traceback = "".join(traceback.format_tb(exc_traceback)) logger.error("Failed %r on data %r" % (err, auth_data)) - logger.error("Function %r, Traceback : %r" % - (fun, formatted_traceback)) + logger.error("Function %r, Traceback : %r" % (fun, formatted_traceback)) return radiusd.RLM_MODULE_FAIL return new_f @@ -128,30 +120,10 @@ def radius_event(fun): @radius_event def instantiate(*_): - """Instantiate api connection - """ + """Usefull for instantiate ldap connexions otherwise, + do nothing""" logger.info("Instantiation") - path = Path(__file__).resolve(strict=True).parent - - config = ConfigParser() - config.read(path / 'config.ini') - - api_hostname = config.get('Re2o', 'hostname') - api_password = config.get('Re2o', 'password') - api_username = config.get('Re2o', 'username') - - def get_api_client(): - """Gets a Re2o, or tries to initialize one""" - if get_api_client.client is None: - get_api_client.client = Re2oAPIClient( - api_hostname, api_username, api_password, use_tls=True) - return get_api_client.client - get_api_client.client = None - - global api_client - api_client = get_api_client - @radius_event def authorize(data): @@ -160,27 +132,20 @@ def authorize(data): - If the nas is known, we apply the 802.1X if enabled, - It the nas is known AND nas auth is enabled with mac address, returns accept here""" - # For proxified request, split - username = username.split("@", 1)[0] - mac = data.get("Calling-Station-Id", "") - - # Get all required objects from API - data_from_api = api_client().view( - "radius/authorize/{0}/{1}/{2}".format( - urllib.parse.quote(nas or "None", safe=""), - urllib.parse.quote(username or "None", safe=""), - urllib.parse.quote(mac or "None", safe="") - )) - - nas_type = data_from_api["nas"] - user = data_from_api["user"] - user_interface = data_from_api["user_interface"] - - if not nas_type or nas_type and nas_type["port_access_mode"] == "802.1X": - result, log, password = check_user_machine_and_register( - nas_type, user, user_interface, nas, username, mac) - logger.info(log.encode("utf-8")) - logger.info(username.encode("utf-8")) + # For proxified request, split + nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) + nas_instance = find_nas_from_request(nas) + # For none proxified requests + nas_type = None + if nas_instance: + nas_type = Nas.objects.filter(nas_type=nas_instance.machine_type).first() + if not nas_type or nas_type.port_access_mode == "802.1X": + user = data.get("User-Name", "") + user = user.split("@", 1)[0] + mac = data.get("Calling-Station-Id", "") + result, log, password = check_user_machine_and_register(nas_type, user, mac) + logger.info(str(log)) + logger.info(str(user)) if not result: return radiusd.RLM_MODULE_REJECT @@ -197,48 +162,54 @@ def authorize(data): @radius_event def post_auth(data): - """Function called after the user is authenticated""" + """ Function called after the user is authenticated + """ nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) - nas_port = data.get("NAS-Port-Id", data.get("NAS-Port", None)) - mac = data.get("Calling-Station-Id", None) - - # Get all required objects from API - data_from_api = api_client().view( - "radius/post_auth/{0}/{1}/{2}".format( - urllib.parse.quote(nas or "None", safe=""), - urllib.parse.quote(nas_port or "None", safe=""), - urllib.parse.quote(mac or "None", safe="") - )) - - nas_type = data_from_api["nas"] - port = data_from_api["port"] - switch = data_from_api["switch"] - - # If proxified request - if not nas_type: + nas_instance = find_nas_from_request(nas) + # All non proxified requests + if not nas_instance: logger.info("Proxified request, nas unknown") return radiusd.RLM_MODULE_OK + nas_type = Nas.objects.filter(nas_type=nas_instance.machine_type).first() + if not nas_type: + logger.info("This kind of nas is not registered in the database!") + return radiusd.RLM_MODULE_OK - # If the request is from a switch (wired connection) - if switch: - # For logging - sw_name = switch["name"] or "?" - room = port["room"] or "Unknown room" if port else "Unknown port" + mac = data.get("Calling-Station-Id", None) - out = decide_vlan_switch(data_from_api, mac, nas_port) - reason, vlan_id, decision, attributes = out + # Switchs and access point can have several interfaces + nas_machine = nas_instance.machine + # If it is a switchs + if hasattr(nas_machine, "switch"): + port = data.get("NAS-Port-Id", data.get("NAS-Port", None)) + # If the switch is part of a stack, calling ip is different from calling switch. + instance_stack = nas_machine.switch.stack + if instance_stack: + # If it is a stack, we select the correct switch in the stack + id_stack_member = port.split("-")[1].split("/")[0] + nas_machine = ( + Switch.objects.filter(stack=instance_stack) + .filter(stack_member_id=id_stack_member) + .prefetch_related("interface_set__domain__extension") + .first() + ) + # Find the port number from freeradius, works both with HP, Cisco + # and juniper output + port = port.split(".")[0].split("/")[-1][-2:] + out = decide_vlan_switch(nas_machine, nas_type, port, mac) + sw_name, room, reason, vlan_id, decision, attributes = out if decision: log_message = "(wired) %s -> %s [%s%s]" % ( - sw_name + ":" + nas_port + "/" + str(room), + sw_name + ":" + port + "/" + str(room), mac, vlan_id, (reason and ": " + reason), ) logger.info(log_message) - # Apply vlan from decide_vlan_switch + # Wired connexion return ( radiusd.RLM_MODULE_UPDATED, ( @@ -250,8 +221,8 @@ def post_auth(data): (), ) else: - log_message = "(wired) %s -> %s [Reject %s]" % ( - sw_name + ":" + nas_port + "/" + str(room), + log_message = "(fil) %s -> %s [Reject %s]" % ( + sw_name + ":" + port + "/" + str(room), mac, (reason and ": " + reason), ) @@ -259,132 +230,113 @@ def post_auth(data): return (radiusd.RLM_MODULE_REJECT, tuple(attributes), ()) - # Else it is from wifi else: return radiusd.RLM_MODULE_OK -def check_user_machine_and_register(nas_type, user, user_interface, nas_id, username, mac_address): +# TODO : remove this function +@radius_event +def dummy_fun(_): + """Do nothing, successfully. """ + return radiusd.RLM_MODULE_OK + + +def detach(_=None): + """Detatch the auth""" + print("*** goodbye from auth.py ***") + return radiusd.RLM_MODULE_OK + + +def find_nas_from_request(nas_id): + """ Get the nas object from its ID """ + nas = ( + Interface.objects.filter( + Q(domain=Domain.objects.filter(name=nas_id)) + | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + ) + .select_related("machine_type") + .select_related("machine__switch__stack") + ) + return nas.first() + + +def check_user_machine_and_register(nas_type, username, mac_address): """Check if username and mac are registered. Register it if unknown. Return the user ntlm password if everything is ok. - Used for 802.1X auth - """ - + Used for 802.1X auth""" + interface = Interface.objects.filter(mac_address=mac_address).first() + user = User.objects.filter(pseudo__iexact=username).first() if not user: - # No username provided return (False, "User unknown", "") - - if not user["access"]: + if not user.has_access(): return (False, "Invalid connexion (non-contributing user)", "") - - if user_interface: - if user_interface["user_pk"] != user["pk"]: + if interface: + if interface.machine.user != user: return ( False, "Mac address registered on another user account", "", ) - - elif not user_interface["active"]: + elif not interface.is_active: return (False, "Interface/Machine disabled", "") - - elif not user_interface["ipv4"]: - # Try to autoassign ip - try: - api_client().view( - "radius/assign_ip/{0}".format( - urllib.parse.quote(mac_address or "None", safe="") - )) - return (True, "Ok, new ipv4 assignement...", user.get("pwd_ntlm", "")) - except HTTPError as err: - return (False, "Error during ip assignement %s" % err.response.text, "") + elif not interface.ipv4: + interface.assign_ipv4() + return (True, "Ok, new ipv4 assignement...", user.pwd_ntlm) else: - return (True, "Access ok", user.get("pwd_ntlm", "")) - + return (True, "Access ok", user.pwd_ntlm) elif nas_type: - # The interface is not yet registred, try to autoregister if enabled - if nas_type["autocapture_mac"]: - try: - api_client().view( - "radius/autoregister/{0}/{1}/{2}".format( - urllib.parse.quote(nas_id or "None", safe=""), - urllib.parse.quote(username or "None", safe=""), - urllib.parse.quote(mac_address or "None", safe="") - )) - return (True, "Access Ok, Registering mac...", user["pwd_ntlm"]) - except HTTPError as err: - return (False, "Error during mac register %s" % err.response.text, "") - return (False, "Autoregistering is disabled", "") + if nas_type.autocapture_mac: + result, reason = user.autoregister_machine(mac_address, nas_type) + if result: + return (True, "Access Ok, Registering mac...", user.pwd_ntlm) + else: + return (False, "Error during mac register %s" % reason, "") else: return (False, "Unknown interface/machine", "") else: return (False, "Unknown interface/machine", "") -def set_radius_attributes_values(attributes, values): - """Set values of parameters in radius attributes""" - return ( - (str(attribute["attribute"]), str(attribute["value"] % values)) - for attribute in attributes - ) - - -def decide_vlan_switch(data_from_api, user_mac, nas_port): +def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): """Function for selecting vlan for a switch with wired mac auth radius. - Two modes exist : in strict mode, a registered user cannot connect with - their machines in a non-registered user room - Sequentially : + Several modes are available : - all modes: - - unknown NAS : VLAN_OK, - - unknown port : Decision set in Re2o RadiusOption - - No radius on this port : VLAN_OK - - force : replace VLAN_OK with vlan provided by the database + - unknown NAS : VLAN_OK, + - unknown port : Decision set in Re2o RadiusOption + - No radius on this port : VLAN_OK + - force : returns vlan provided by the database - mode strict: - no room : Decision set in Re2o RadiusOption, - no user in this room : Reject, - user of this room is banned or disable : Reject, - - user of this room non-contributor and not whitelisted: + - user of this room non-contributor and not whitelisted: Decision set in Re2o RadiusOption - mode common : - mac-address already registered: - related user non contributor / interface disabled: - Decision set in Re2o RadiusOption + Decision set in Re2o RadiusOption - related user is banned: - Decision set in Re2o RadiusOption + Decision set in Re2o RadiusOption - user contributing : VLAN_OK (can assign ipv4 if needed) - unknown interface : - register mac disabled : Decision set in Re2o RadiusOption - - register mac enabled : redirect to webauth (not implemented) + - register mac enabled : redirect to webauth Returns: tuple with : + - Switch name (str) + - Room (str) - Reason of the decision (str) - vlan_id (int) - decision (bool) - - Other Attributs (attribut:str, value:str) + - Other Attributs (attribut:str, operator:str, value:str) """ - - # Get values from api - nas_type = data_from_api["nas"] - room_users = data_from_api["room_users"] - port = data_from_api["port"] - port_profile = data_from_api["port_profile"] - switch = data_from_api["switch"] - user_interface = data_from_api["user_interface"] - radius_option = data_from_api["radius_option"] - EMAIL_STATE_UNVERIFIED = data_from_api["EMAIL_STATE_UNVERIFIED"] - RADIUS_OPTION_REJECT = data_from_api["RADIUS_OPTION_REJECT"] - USER_STATE_ACTIVE = data_from_api["USER_STATE_ACTIVE"] - - # Values which can be used as parameters in radius attributes attributes_kwargs = { - "client_mac": str(user_mac), - # magic split - "switch_port": str(nas_port.split(".")[0].split("/")[-1][-2:]), - "switch_ip": str(switch["ipv4"]) + "client_mac": str(mac_address), + "switch_port": str(port_number), } - + # Get port from switch and port number extra_log = "" - # If NAS is unknown, go to default vlan + # If NAS is unknown, go to default vlan if not nas_machine: return ( "?", @@ -401,36 +353,41 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): attributes_kwargs["switch_ip"] = str(switch.ipv4) port = Port.objects.filter(switch=switch, port=port_number).first() - # If the port is unknown, do as in RadiusOption - if not port or not port_profile: + # If the port is unknwon, go to default vlan + # We don't have enought information to make a better decision + if not port: return ( + sw_name, "Unknown port", - radius_option["unknown_port_vlan"] and radius_option["unknown_port_vlan"]["vlan_id"] or None, - radius_option["unknown_port"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["unknown_port_attributes"], attributes_kwargs), + "PUnknown port", + getattr( + RadiusOption.get_cached_value("unknown_port_vlan"), "vlan_id", None + ), + RadiusOption.get_cached_value("unknown_port") != RadiusOption.REJECT, + RadiusOption.get_attributes("unknown_port_attributes", attributes_kwargs), ) - + # Retrieve port profile port_profile = port.get_port_profile # If a vlan is precised in port config, we use it - if port_profile["vlan_untagged"]: - DECISION_VLAN = int(port_profile["vlan_untagged"]["vlan_id"]) - extra_log = "Force sur vlan %s" % str(DECISION_VLAN) + if port_profile.vlan_untagged: + DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) + extra_log = "Force sur vlan " + str(DECISION_VLAN) attributes = () else: - DECISION_VLAN = radius_option["vlan_decision_ok"]["vlan_id"] - attributes = set_radius_attributes_values( - radius_option["ok_attributes"], attributes_kwargs) + DECISION_VLAN = RadiusOption.get_cached_value("vlan_decision_ok").vlan_id + attributes = RadiusOption.get_attributes("ok_attributes", attributes_kwargs) # If the port is disabled in re2o, REJECT - if not port["state"]: - return ("Port disabled", None, False, ()) + if not port.state: + return (sw_name, port.room, "Port disabled", None, False, ()) # If radius is disabled, decision is OK - if port_profile["radius_type"] == "NO": + if port_profile.radius_type == "NO": return ( + sw_name, + "", "No Radius auth enabled on this port" + extra_log, DECISION_VLAN, True, @@ -439,8 +396,11 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): # If 802.1X is enabled, people has been previously accepted. # Go to the decision vlan - if (nas_type["port_access_mode"], port_profile["radius_type"]) == ("802.1X", "802.1X"): + if (nas_type.port_access_mode, port_profile.radius_type) == ("802.1X", "802.1X"): + room = port.room or "Room unknown" return ( + sw_name, + room, "Accept authentication 802.1X", DECISION_VLAN, True, @@ -449,123 +409,179 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port): # Otherwise, we are in mac radius. # If strict mode is enabled, we check every user related with this port. If - # all users and clubs are disabled, we reject to prevent from sharing or + # one user or more is not enabled, we reject to prevent from sharing or # spoofing mac. - if port_profile["radius_mode"] == "STRICT": - if not port["room"]: + if port_profile.radius_mode == "STRICT": + room = port.room + if not room: return ( + sw_name, + "Unknown", "Unkwown room", - radius_option["unknown_room_vlan"] and radius_option["unknown_room_vlan"]["vlan_id"] or None, - radius_option["unknown_room"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["unknown_room_attributes"], attributes_kwargs), + getattr( + RadiusOption.get_cached_value("unknown_room_vlan"), "vlan_id", None + ), + RadiusOption.get_cached_value("unknown_room") != RadiusOption.REJECT, + RadiusOption.get_attributes( + "unknown_room_attributes", attributes_kwargs + ), ) - if not room_users: + room_user = User.objects.filter( + Q(club__room=port.room) | Q(adherent__room=port.room) + ) + if not room_user: return ( + sw_name, + room, "Non-contributing room", - radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, - radius_option["non_member"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["non_member_attributes"], attributes_kwargs), - ) - - all_user_ban = True - at_least_one_active_user = False - - for user in room_users: - if not user["is_ban"] and user["state"] == USER_STATE_ACTIVE: - all_user_ban = False - if user["email_state"] != EMAIL_STATE_UNVERIFIED and (user["is_connected"] or user["is_whitelisted"]): - at_least_one_active_user = True - - if all_user_ban: - return ( - "User is banned or disabled", - radius_option["banned_vlan"] and radius_option["banned_vlan"]["vlan_id"] or None, - radius_option["banned"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["banned_attributes"], attributes_kwargs), - ) - if not at_least_one_active_user: - return ( - "Non-contributing member or unconfirmed mail", - radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, - radius_option["non_member"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["non_member_attributes"], attributes_kwargs), + getattr( + RadiusOption.get_cached_value("non_member_vlan"), "vlan_id", None + ), + RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, + RadiusOption.get_attributes("non_member_attributes", attributes_kwargs), ) + for user in room_user: + if user.is_ban() or user.state != User.STATE_ACTIVE: + return ( + sw_name, + room, + "User is banned or disabled", + getattr( + RadiusOption.get_cached_value("banned_vlan"), "vlan_id", None + ), + RadiusOption.get_cached_value("banned") != RadiusOption.REJECT, + RadiusOption.get_attributes("banned_attributes", attributes_kwargs), + ) + elif user.email_state == User.EMAIL_STATE_UNVERIFIED: + return ( + sw_name, + room, + "User is suspended (mail has not been confirmed)", + getattr( + RadiusOption.get_cached_value("non_member_vlan"), + "vlan_id", + None, + ), + RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, + RadiusOption.get_attributes( + "non_member_attributes", attributes_kwargs + ), + ) + elif not (user.is_connected() or user.is_whitelisted()): + return ( + sw_name, + room, + "Non-contributing member", + getattr( + RadiusOption.get_cached_value("non_member_vlan"), + "vlan_id", + None, + ), + RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, + RadiusOption.get_attributes( + "non_member_attributes", attributes_kwargs + ), + ) # else: user OK, so we check MAC now - # If mac is unknown, - if not user_interface: - # We try to register mac, if autocapture is enabled - # Final decision depend on RADIUSOption set in re2o - # Something is not implemented here... - if nas_type["autocapture_mac"]: - return ( - "Unknown mac/interface", - radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, - radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["unknown_machine_attributes"], attributes_kwargs), - ) - # Otherwise, if autocapture mac is not enabled, - else: - return ( - "Unknown mac/interface", - radius_option["unknown_machine_vlan"] and radius_option["unknown_machine_vlan"]["vlan_id"] or None, - radius_option["unknown_machine"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["unknown_machine_attributes"], attributes_kwargs), - ) - - # Mac/Interface is found, check if related user is contributing and ok - # If needed, set ipv4 to it - else: - if user_interface["is_ban"]: - return ( - "Banned user", - radius_option["banned_vlan"] and radius_option["banned_vlan"]["vlan_id"] or None, - radius_option["banned"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["banned_attributes"], attributes_kwargs), - ) - if not user_interface["active"]: - return ( - "Disabled interface / non-contributing member", - radius_option["non_member_vlan"] and radius_option["non_member_vlan"]["vlan_id"] or None, - radius_option["non_member"] != RADIUS_OPTION_REJECT, - set_radius_attributes_values( - radius_option["non_member_attributes"], attributes_kwargs), - ) - # If settings is set to related interface vlan policy based on interface type: - if radius_option["radius_general_policy"] == "MACHINE": - DECISION_VLAN = user_interface["vlan_id"] - if not user_interface["ipv4"]: - try: - api_client().view( - "radius/assign_ip/{0}".format( - urllib.parse.quote(user_mac or "None", safe="") - )) + # If we are authenticating with mac, we look for the interfaces and its mac address + if port_profile.radius_mode == "COMMON" or port_profile.radius_mode == "STRICT": + # Mac auth + interface = ( + Interface.objects.filter(mac_address=mac_address) + .select_related("machine__user") + .select_related("ipv4") + .first() + ) + # If mac is unknown, + if not interface: + room = port.room + # We try to register mac, if autocapture is enabled + # Final decision depend on RADIUSOption set in re2o + if nas_type.autocapture_mac: return ( + sw_name, + room, + "Unknown mac/interface", + getattr( + RadiusOption.get_cached_value("unknown_machine_vlan"), + "vlan_id", + None, + ), + RadiusOption.get_cached_value("unknown_machine") + != RadiusOption.REJECT, + RadiusOption.get_attributes( + "unknown_machine_attributes", attributes_kwargs + ), + ) + # Otherwise, if autocapture mac is not enabled, + else: + return ( + sw_name, + "", + "Unknown mac/interface", + getattr( + RadiusOption.get_cached_value("unknown_machine_vlan"), + "vlan_id", + None, + ), + RadiusOption.get_cached_value("unknown_machine") + != RadiusOption.REJECT, + RadiusOption.get_attributes( + "unknown_machine_attributes", attributes_kwargs + ), + ) + + # Mac/Interface is found, check if related user is contributing and ok + # If needed, set ipv4 to it + else: + room = port.room + if interface.machine.user.is_ban(): + return ( + sw_name, + room, + "Banned user", + getattr( + RadiusOption.get_cached_value("banned_vlan"), "vlan_id", None + ), + RadiusOption.get_cached_value("banned") != RadiusOption.REJECT, + RadiusOption.get_attributes("banned_attributes", attributes_kwargs), + ) + if not interface.is_active: + return ( + sw_name, + room, + "Disabled interface / non-contributing member", + getattr( + RadiusOption.get_cached_value("non_member_vlan"), + "vlan_id", + None, + ), + RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT, + RadiusOption.get_attributes( + "non_member_attributes", attributes_kwargs + ), + ) + # If settings is set to related interface vlan policy based on interface type: + if RadiusOption.get_cached_value("radius_general_policy") == "MACHINE": + DECISION_VLAN = interface.machine_type.ip_type.vlan.vlan_id + if not interface.ipv4: + interface.assign_ipv4() + return ( + sw_name, + room, "Ok, assigning new ipv4" + extra_log, DECISION_VLAN, True, attributes, ) - except HTTPError as err: + else: return ( - "Error during ip assignement %s" % err.response.text + extra_log, + sw_name, + room, + "Interface OK" + extra_log, DECISION_VLAN, True, attributes, ) - - else: - return ( - "Interface OK" + extra_log, - DECISION_VLAN, - True, - attributes, - ) From a234be098128743fd5ed163509d68ff65ab62acd Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:43:39 +0200 Subject: [PATCH 12/22] linter --- api/permissions.py | 9 +++++---- freeradius_utils/auth.py | 28 +++++++++++++--------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/api/permissions.py b/api/permissions.py index 9f120bec..4061d7d7 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -213,7 +213,7 @@ class AutodetectACLPermission(permissions.BasePermission): return [perm(obj) for perm in self.perms_obj_map[method]] - @ staticmethod + @staticmethod def _queryset(view): return _get_param_in_view(view, "queryset") @@ -240,7 +240,9 @@ class AutodetectACLPermission(permissions.BasePermission): if getattr(view, "_ignore_model_permissions", False): return True - if not getattr(view, "queryset", None): + # Bypass permission verifications if it is a functional view + # (permissions are handled by ACL) + if not hasattr(view, "queryset") and not hasattr(view, "get_queryset"): return True if not request.user or not request.user.is_authenticated: @@ -277,8 +279,7 @@ class AutodetectACLPermission(permissions.BasePermission): # they have read permissions to see 403, or not, and simply see # a 404 response. - SAFE_METHODS = ("GET", "OPTIONS", "HEAD", - "POST", "PUT", "PATCH", "DELETE") + SAFE_METHODS = ("GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE") if request.method in SAFE_METHODS: # Read permissions already checked and failed, no need diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index f4201f44..d41e9a5a 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -34,12 +34,12 @@ https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_pyth Inspired by Daniel Stan in Crans """ +import logging import os import sys -import logging import traceback -import radiusd # Magic module freeradius (radiusd.py is dummy) +import radiusd # Magic module freeradius (radiusd.py is dummy) from django.core.wsgi import get_wsgi_application from django.db.models import Q @@ -54,11 +54,10 @@ os.chdir(proj_path) # This is so models get loaded. application = get_wsgi_application() -from machines.models import Interface, IpList, Nas, Domain +from machines.models import Domain, Interface, IpList, Nas +from preferences.models import RadiusOption from topologie.models import Port, Switch from users.models import User -from preferences.models import RadiusOption - # Logging @@ -76,7 +75,7 @@ class RadiusdHandler(logging.Handler): radiusd.radlog(rad_sig, str(record.msg)) -# Init for logging +# Init for logging logger = logging.getLogger("auth.py") logger.setLevel(logging.DEBUG) formatter = logging.Formatter("%(name)s: [%(levelname)s] %(message)s") @@ -97,7 +96,7 @@ def radius_event(fun): """ def new_f(auth_data): - """ The function transforming the tuples as dict """ + """The function transforming the tuples as dict """ if isinstance(auth_data, dict): data = auth_data else: @@ -132,10 +131,10 @@ def authorize(data): - If the nas is known, we apply the 802.1X if enabled, - It the nas is known AND nas auth is enabled with mac address, returns accept here""" - # For proxified request, split + # For proxified request, split nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) nas_instance = find_nas_from_request(nas) - # For none proxified requests + # For none proxified requests nas_type = None if nas_instance: nas_type = Nas.objects.filter(nas_type=nas_instance.machine_type).first() @@ -162,12 +161,11 @@ def authorize(data): @radius_event def post_auth(data): - """ Function called after the user is authenticated - """ + """ Function called after the user is authenticated""" nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) nas_instance = find_nas_from_request(nas) - # All non proxified requests + # All non proxified requests if not nas_instance: logger.info("Proxified request, nas unknown") return radiusd.RLM_MODULE_OK @@ -309,7 +307,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): - no room : Decision set in Re2o RadiusOption, - no user in this room : Reject, - user of this room is banned or disable : Reject, - - user of this room non-contributor and not whitelisted: + - user of this room non-contributor and not whitelisted: Decision set in Re2o RadiusOption - mode common : - mac-address already registered: @@ -336,7 +334,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): } # Get port from switch and port number extra_log = "" - # If NAS is unknown, go to default vlan + # If NAS is unknown, go to default vlan if not nas_machine: return ( "?", @@ -366,7 +364,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): RadiusOption.get_cached_value("unknown_port") != RadiusOption.REJECT, RadiusOption.get_attributes("unknown_port_attributes", attributes_kwargs), ) - + # Retrieve port profile port_profile = port.get_port_profile From 1e6c6348b82999849c4d7f25bcdb2de19b2cb4a1 Mon Sep 17 00:00:00 2001 From: chapeau Date: Thu, 13 May 2021 19:46:26 +0200 Subject: [PATCH 13/22] add slash support in apache conf --- install_utils/apache2/re2o-tls.conf | 2 ++ install_utils/apache2/re2o.conf | 2 ++ 2 files changed, 4 insertions(+) diff --git a/install_utils/apache2/re2o-tls.conf b/install_utils/apache2/re2o-tls.conf index eb8f2c42..b9ae3741 100644 --- a/install_utils/apache2/re2o-tls.conf +++ b/install_utils/apache2/re2o-tls.conf @@ -32,4 +32,6 @@ SSLCertificateKeyFile /etc/letsencrypt/live/LE_PATH/privkey.pem Include /etc/letsencrypt/options-ssl-apache.conf + AllowEncodedSlashes On + diff --git a/install_utils/apache2/re2o.conf b/install_utils/apache2/re2o.conf index 1b4e02b3..ed2bbf4c 100644 --- a/install_utils/apache2/re2o.conf +++ b/install_utils/apache2/re2o.conf @@ -21,4 +21,6 @@ WSGIDaemonProcess re2o processes=2 threads=16 maximum-requests=1000 display-name=re2o WSGIPassAuthorization On + AllowEncodedSlashes On + From 2381ae2169b246f51320b85b07651a93067af88f Mon Sep 17 00:00:00 2001 From: chapeau Date: Fri, 14 May 2021 10:32:17 +0000 Subject: [PATCH 14/22] Update views.py --- radius/api/views.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/radius/api/views.py b/radius/api/views.py index 2f10f916..4e472d04 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -43,11 +43,6 @@ class AuthorizeResponse: self.user = user self.user_interface = user_interface - def can_view(self, user): - """Temp method to bypass ACL - """ - return [True] - @api_view(['GET']) @login_required @@ -103,9 +98,6 @@ class PostAuthResponse: self.RADIUS_OPTION_REJECT = RADIUS_OPTION_REJECT self.USER_STATE_ACTIVE = USER_STATE_ACTIVE - def can_view(self, user): - return [True] - @api_view(['GET']) @login_required From 303defa16da9a550d370327d7f660e9b9207e738 Mon Sep 17 00:00:00 2001 From: chapeau Date: Fri, 14 May 2021 10:58:05 +0000 Subject: [PATCH 15/22] remove print --- radius/api/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/radius/api/views.py b/radius/api/views.py index 4e472d04..272cc4d9 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -170,7 +170,6 @@ def post_auth(request, nas_id, nas_port, user_mac): # get all radius options radius_option = RadiusOption.objects.first() - print(radius_option) # get a few class constants the radius will need EMAIL_STATE_UNVERIFIED = User.EMAIL_STATE_UNVERIFIED From e4789c6d8bd85a4eb2fbecebf2c44410a9e2a256 Mon Sep 17 00:00:00 2001 From: chapeau Date: Fri, 14 May 2021 11:05:32 +0000 Subject: [PATCH 16/22] documentation --- freeradius_utils/auth.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index d41e9a5a..d1876edd 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -185,6 +185,11 @@ def post_auth(data): instance_stack = nas_machine.switch.stack if instance_stack: # If it is a stack, we select the correct switch in the stack + # + # For Juniper, the result looks something like this: NAS-Port-Id = "ge-0/0/6.0"" + # For other brands (e.g. HP or Mikrotik), the result usually looks like: NAS-Port-Id = "6.0" + # This "magic split" handles both cases + # Cisco can rot in Hell for all I care, so their format is not supported (it looks like NAS-Port-ID = atm 31/31/7:255.65535 guangzhou001/0/31/63/31/127) id_stack_member = port.split("-")[1].split("/")[0] nas_machine = ( Switch.objects.filter(stack=instance_stack) @@ -192,8 +197,12 @@ def post_auth(data): .prefetch_related("interface_set__domain__extension") .first() ) - # Find the port number from freeradius, works both with HP, Cisco - # and juniper output + # Find the port number from freeradius + # + # For Juniper, the result looks something like this: NAS-Port-Id = "ge-0/0/6.0"" + # For other brands (e.g. HP or Mikrotik), the result usually looks like: NAS-Port-Id = "6.0" + # This "magic split" handles both cases + # Cisco can rot in Hell for all I care, so their format is not supported (it looks like NAS-Port-ID = atm 31/31/7:255.65535 guangzhou001/0/31/63/31/127) port = port.split(".")[0].split("/")[-1][-2:] out = decide_vlan_switch(nas_machine, nas_type, port, mac) sw_name, room, reason, vlan_id, decision, attributes = out From 2a6c09c808d893ef5ebad7ee8a6e53cd08387e78 Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 16 May 2021 22:29:39 +0200 Subject: [PATCH 17/22] add app name --- radius/urls.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/radius/urls.py b/radius/urls.py index 4271f701..3141fb90 100644 --- a/radius/urls.py +++ b/radius/urls.py @@ -1,4 +1,26 @@ +# -*- mode: python; coding: utf-8 -*- +# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2021 Corentin Canebier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + from . import views urlpatterns = [] +app_name = "radius" \ No newline at end of file From c15ec606c6af3cdadcf2c68878ed7c800e21bb5e Mon Sep 17 00:00:00 2001 From: chapeau Date: Mon, 17 May 2021 13:48:07 +0200 Subject: [PATCH 18/22] adapt radius api for django 2 --- radius/api/views.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/radius/api/views.py b/radius/api/views.py index 272cc4d9..ebc65009 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -43,6 +43,9 @@ class AuthorizeResponse: self.user = user self.user_interface = user_interface + def can_view(self, user): + return [True] + @api_view(['GET']) @login_required @@ -61,8 +64,8 @@ def authorize(request, nas_id, username, mac_address): # get the Nas object which made the request (if exists) nas_interface = Interface.objects.filter( - Q(domain=Domain.objects.filter(name=nas_id)) - | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + Q(domain__name=nas_id) + | Q(ipv4__ipv4=nas_id) ).first() nas_type = None if nas_interface: @@ -98,6 +101,9 @@ class PostAuthResponse: self.RADIUS_OPTION_REJECT = RADIUS_OPTION_REJECT self.USER_STATE_ACTIVE = USER_STATE_ACTIVE + def can_view(self, user): + return [True] + @api_view(['GET']) @login_required @@ -116,8 +122,8 @@ def post_auth(request, nas_id, nas_port, user_mac): # get the Nas object which made the request (if exists) nas_interface = Interface.objects.prefetch_related("machine__switch__stack").filter( - Q(domain=Domain.objects.filter(name=nas_id)) - | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + Q(domain__name=nas_id) + | Q(ipv4__ipv4=nas_id) ).first() nas_type = None if nas_interface: @@ -199,8 +205,8 @@ def autoregister_machine(request, nas_id, username, mac_address): 400 if it failed, and the reason why """ nas_interface = Interface.objects.filter( - Q(domain=Domain.objects.filter(name=nas_id)) - | Q(ipv4=IpList.objects.filter(ipv4=nas_id)) + Q(domain__name=nas_id) + | Q(ipv4__ipv4=nas_id) ).first() nas_type = None if nas_interface: From c5e21e05ddb49d0760eb0a806900e526e8980013 Mon Sep 17 00:00:00 2001 From: chapeau Date: Mon, 17 May 2021 16:47:53 +0200 Subject: [PATCH 19/22] documentation --- radius/api/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/radius/api/views.py b/radius/api/views.py index ebc65009..24857aa2 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -44,7 +44,9 @@ class AuthorizeResponse: self.user_interface = user_interface def can_view(self, user): - return [True] + """Method to bypass api permissions, because we are using ACL decorators + """ + return (True, None, None) @api_view(['GET']) @@ -102,7 +104,9 @@ class PostAuthResponse: self.USER_STATE_ACTIVE = USER_STATE_ACTIVE def can_view(self, user): - return [True] + """Method to bypass api permissions, because we are using ACL decorators + """ + return (True, None, None) @api_view(['GET']) From 118f5af2695185a5b5b6b78538320df45753f5c1 Mon Sep 17 00:00:00 2001 From: chapeau Date: Mon, 17 May 2021 21:59:33 +0200 Subject: [PATCH 20/22] black --- freeradius_utils/auth.py | 4 +- radius/api/serializers.py | 5 ++- radius/api/urls.py | 21 +++++++--- radius/api/views.py | 83 ++++++++++++++++++++++----------------- radius/urls.py | 2 +- 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index d1876edd..dc052a78 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -96,7 +96,7 @@ def radius_event(fun): """ def new_f(auth_data): - """The function transforming the tuples as dict """ + """ The function transforming the tuples as dict """ if isinstance(auth_data, dict): data = auth_data else: @@ -161,7 +161,7 @@ def authorize(data): @radius_event def post_auth(data): - """ Function called after the user is authenticated""" + """Function called after the user is authenticated""" nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None)) nas_instance = find_nas_from_request(nas) diff --git a/radius/api/serializers.py b/radius/api/serializers.py index 867f7a54..febca280 100644 --- a/radius/api/serializers.py +++ b/radius/api/serializers.py @@ -38,8 +38,7 @@ class InterfaceSerializer(Serializer): user_pk = serializers.CharField(source="machine.user.pk") machine_short_name = serializers.CharField(source="machine.short_name") is_ban = serializers.BooleanField(source="machine.user.is_ban") - vlan_id = serializers.IntegerField( - source="machine_type.ip_type.vlan.vlan_id") + vlan_id = serializers.IntegerField(source="machine_type.ip_type.vlan.vlan_id") class NasSerializer(Serializer): @@ -108,6 +107,7 @@ class AuthorizeResponseSerializer(Serializer): """Serializer for AuthorizeResponse objects See views.py for the declaration of AuthorizeResponse """ + nas = NasSerializer(read_only=True) user = UserSerializer(read_only=True) user_interface = InterfaceSerializer(read_only=True) @@ -117,6 +117,7 @@ class PostAuthResponseSerializer(Serializer): """Serializer for PostAuthResponse objects See views.py for the declaration of PostAuthResponse """ + nas = NasSerializer(read_only=True) room_users = UserSerializer(many=True) port = PortSerializer() diff --git a/radius/api/urls.py b/radius/api/urls.py index d24b89eb..3e141936 100644 --- a/radius/api/urls.py +++ b/radius/api/urls.py @@ -22,11 +22,20 @@ from . import views urls_functional_view = [ - (r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", - views.authorize, None), - (r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", - views.post_auth, None), - (r"radius/autoregister/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", - views.autoregister_machine, None), + ( + r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + views.authorize, + None, + ), + ( + r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + views.post_auth, + None, + ), + ( + r"radius/autoregister/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + views.autoregister_machine, + None, + ), (r"radius/assign_ip/(?P[0-9a-fA-F\:\-]{17})$", views.assign_ip, None), ] diff --git a/radius/api/views.py b/radius/api/views.py index 24857aa2..6c56073d 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -35,8 +35,7 @@ from re2o.acl import can_view_all_api, can_edit_all_api, can_create_api class AuthorizeResponse: - """Contains objects the radius needs for the Authorize step - """ + """Contains objects the radius needs for the Authorize step""" def __init__(self, nas, user, user_interface): self.nas = nas @@ -44,12 +43,11 @@ class AuthorizeResponse: self.user_interface = user_interface def can_view(self, user): - """Method to bypass api permissions, because we are using ACL decorators - """ + """Method to bypass api permissions, because we are using ACL decorators""" return (True, None, None) -@api_view(['GET']) +@api_view(["GET"]) @login_required @can_view_all_api(Interface, Domain, IpList, Nas, User) def authorize(request, nas_id, username, mac_address): @@ -66,13 +64,11 @@ def authorize(request, nas_id, username, mac_address): # get the Nas object which made the request (if exists) nas_interface = Interface.objects.filter( - Q(domain__name=nas_id) - | Q(ipv4__ipv4=nas_id) + Q(domain__name=nas_id) | Q(ipv4__ipv4=nas_id) ).first() nas_type = None if nas_interface: - nas_type = Nas.objects.filter( - nas_type=nas_interface.machine_type).first() + nas_type = Nas.objects.filter(nas_type=nas_interface.machine_type).first() # get the User corresponding to the username in the URL # If no username was provided (wired connection), username="None" @@ -82,16 +78,28 @@ def authorize(request, nas_id, username, mac_address): user_interface = Interface.objects.filter(mac_address=mac_address).first() serialized = serializers.AuthorizeResponseSerializer( - AuthorizeResponse(nas_type, user, user_interface)) + AuthorizeResponse(nas_type, user, user_interface) + ) return Response(data=serialized.data) class PostAuthResponse: - """Contains objects the radius needs for the Post-Auth step - """ + """Contains objects the radius needs for the Post-Auth step""" - def __init__(self, nas, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE): + def __init__( + self, + nas, + room_users, + port, + port_profile, + switch, + user_interface, + radius_option, + EMAIL_STATE_UNVERIFIED, + RADIUS_OPTION_REJECT, + USER_STATE_ACTIVE, + ): self.nas = nas self.room_users = room_users self.port = port @@ -104,12 +112,11 @@ class PostAuthResponse: self.USER_STATE_ACTIVE = USER_STATE_ACTIVE def can_view(self, user): - """Method to bypass api permissions, because we are using ACL decorators - """ + """Method to bypass api permissions, because we are using ACL decorators""" return (True, None, None) -@api_view(['GET']) +@api_view(["GET"]) @login_required @can_view_all_api(Interface, Domain, IpList, Nas, Switch, Port, User) def post_auth(request, nas_id, nas_port, user_mac): @@ -125,20 +132,19 @@ def post_auth(request, nas_id, nas_port, user_mac): """ # get the Nas object which made the request (if exists) - nas_interface = Interface.objects.prefetch_related("machine__switch__stack").filter( - Q(domain__name=nas_id) - | Q(ipv4__ipv4=nas_id) - ).first() + nas_interface = ( + Interface.objects.prefetch_related("machine__switch__stack") + .filter(Q(domain__name=nas_id) | Q(ipv4__ipv4=nas_id)) + .first() + ) nas_type = None if nas_interface: - nas_type = Nas.objects.filter( - nas_type=nas_interface.machine_type).first() + nas_type = Nas.objects.filter(nas_type=nas_interface.machine_type).first() # get the switch (if wired connection) switch = None if nas_interface: - switch = Switch.objects.filter( - machine_ptr=nas_interface.machine).first() + switch = Switch.objects.filter(machine_ptr=nas_interface.machine).first() # If the switch is part of a stack, get the correct object if hasattr(nas_interface.machine, "switch"): @@ -187,12 +193,24 @@ def post_auth(request, nas_id, nas_port, user_mac): USER_STATE_ACTIVE = User.STATE_ACTIVE serialized = serializers.PostAuthResponseSerializer( - PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE)) + PostAuthResponse( + nas_type, + room_users, + port, + port_profile, + switch, + user_interface, + radius_option, + EMAIL_STATE_UNVERIFIED, + RADIUS_OPTION_REJECT, + USER_STATE_ACTIVE, + ) + ) return Response(data=serialized.data) -@api_view(['GET']) +@api_view(["GET"]) @login_required @can_view_all_api(Interface, Domain, IpList, Nas, User) @can_edit_all_api(User, Domain, Machine, Interface) @@ -209,13 +227,11 @@ def autoregister_machine(request, nas_id, username, mac_address): 400 if it failed, and the reason why """ nas_interface = Interface.objects.filter( - Q(domain__name=nas_id) - | Q(ipv4__ipv4=nas_id) + Q(domain__name=nas_id) | Q(ipv4__ipv4=nas_id) ).first() nas_type = None if nas_interface: - nas_type = Nas.objects.filter( - nas_type=nas_interface.machine_type).first() + nas_type = Nas.objects.filter(nas_type=nas_interface.machine_type).first() user = User.objects.filter(pseudo__iexact=username).first() @@ -225,7 +241,7 @@ def autoregister_machine(request, nas_id, username, mac_address): return Response(reason, status=400) -@api_view(['GET']) +@api_view(["GET"]) @can_view_all_api(Interface) @can_edit_all_api(Interface) def assign_ip(request, mac_address): @@ -238,10 +254,7 @@ def assign_ip(request, mac_address): 200 if it worked 400 if it failed, and the reason why """ - interface = ( - Interface.objects.filter(mac_address=mac_address) - .first() - ) + interface = Interface.objects.filter(mac_address=mac_address).first() try: interface.assign_ipv4() diff --git a/radius/urls.py b/radius/urls.py index 3141fb90..c412a679 100644 --- a/radius/urls.py +++ b/radius/urls.py @@ -23,4 +23,4 @@ from . import views urlpatterns = [] -app_name = "radius" \ No newline at end of file +app_name = "radius" From cf5756733d007584d297e52001e4781a41eb37ac Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Thu, 20 May 2021 23:20:51 +0200 Subject: [PATCH 21/22] Tweak "magic split" comments --- freeradius_utils/auth.py | 6 +----- radius/api/views.py | 7 +++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index dc052a78..9c1fd295 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -198,11 +198,7 @@ def post_auth(data): .first() ) # Find the port number from freeradius - # - # For Juniper, the result looks something like this: NAS-Port-Id = "ge-0/0/6.0"" - # For other brands (e.g. HP or Mikrotik), the result usually looks like: NAS-Port-Id = "6.0" - # This "magic split" handles both cases - # Cisco can rot in Hell for all I care, so their format is not supported (it looks like NAS-Port-ID = atm 31/31/7:255.65535 guangzhou001/0/31/63/31/127) + # See above for details about this "magic split" port = port.split(".")[0].split("/")[-1][-2:] out = decide_vlan_switch(nas_machine, nas_type, port, mac) sw_name, room, reason, vlan_id, decision, attributes = out diff --git a/radius/api/views.py b/radius/api/views.py index 6c56073d..ba490b04 100644 --- a/radius/api/views.py +++ b/radius/api/views.py @@ -150,7 +150,10 @@ def post_auth(request, nas_id, nas_port, user_mac): if hasattr(nas_interface.machine, "switch"): stack = nas_interface.machine.switch.stack if stack: - # magic split + # For Juniper, the result looks something like this: NAS-Port-Id = "ge-0/0/6.0"" + # For other brands (e.g. HP or Mikrotik), the result usually looks like: NAS-Port-Id = "6.0" + # This "magic split" handles both cases + # Cisco can rot in Hell for all I care, so their format is not supported (it looks like NAS-Port-ID = atm 31/31/7:255.65535 guangzhou001/0/31/63/31/127) id_stack_member = nas_port.split("-")[1].split("/")[0] switch = ( Switch.objects.filter(stack=stack) @@ -161,7 +164,7 @@ def post_auth(request, nas_id, nas_port, user_mac): # get the switch port port = None if nas_port and nas_port != "None": - # magic split + # magic split (see above) port_number = nas_port.split(".")[0].split("/")[-1][-2:] port = Port.objects.filter(switch=switch, port=port_number).first() From 5a5fb1befdae9632c59541b307fd5e7e0fe20983 Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 23 May 2021 10:45:40 +0200 Subject: [PATCH 22/22] change regex to remove a dos vuln --- radius/api/urls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/radius/api/urls.py b/radius/api/urls.py index 3e141936..2ab8ec91 100644 --- a/radius/api/urls.py +++ b/radius/api/urls.py @@ -23,17 +23,17 @@ from . import views urls_functional_view = [ ( - r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + r"radius/authorize/(?P[^/]+)/(?P.+)/(?P[^/]{17})$", views.authorize, None, ), ( - r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + r"radius/post_auth/(?P[^/]+)/(?P.+)/(?P[^/]{17})$", views.post_auth, None, ), ( - r"radius/autoregister/(?P[^/]+)/(?P.+)/(?P[0-9a-fA-F\:\-]{17})$", + r"radius/autoregister/(?P[^/]+)/(?P.+)/(?P[^/]{17})$", views.autoregister_machine, None, ),