diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py new file mode 100644 index 00000000..0c441e40 --- /dev/null +++ b/freeradius_utils/auth.py @@ -0,0 +1,217 @@ +# ⁻*- mode: python; coding: utf-8 -*- +""" +Backend python pour freeradius. + +Ce fichier contient la définition de plusieurs fonctions d'interface à +freeradius qui peuvent être appelées (suivant les configurations) à certains +moment de l'authentification, en WiFi, filaire, ou par les NAS eux-mêmes. + +Inspirés d'autres exemples trouvés ici : +https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ +""" + +import logging +import netaddr +import radiusd # Module magique freeradius (radiusd.py is dummy) +import os +import binascii +import hashlib +import subprocess + +import os, sys +from ast import literal_eval as make_tuple + +#: Serveur radius de test (pas la prod) +TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) + + +## -*- Logging -*- + +class RadiusdHandler(logging.Handler): + """Handler de logs pour freeradius""" + + def emit(self, record): + """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, record.msg) + +# Initialisation d'un logger (pour logguer unifié) +logger = logging.getLogger('auth.py') +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s') +handler = RadiusdHandler() +handler.setFormatter(formatter) +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) + + 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): + if type(auth_data) == dict: + data = auth_data + else: + data = dict() + for (key, value) in auth_data or []: + # Beware: les valeurs scalaires sont entre guillemets + # 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: + logger.error('Failed %r on data %r' % (err, auth_data)) + raise + + return new_f + + + +@radius_event +def instantiate(*_): + """Utile pour initialiser les connexions ldap une première fois (otherwise, + do nothing)""" + logger.info('Instantiation') + if TEST_SERVER: + logger.info('DBG_FREERADIUS is enabled') + +@radius_event +def authorize(data): + """Fonction qui aiguille entre nas, wifi et filaire pour authorize + On se contecte de faire une verification basique de ce que contien la requète + pour déterminer la fonction à utiliser""" + if data.get('Service-Type', '')==u'NAS-Prompt-User' or data.get('Service-Type', '')==u'Administrative-User': + return authorize_user(data) + else: + return authorize_fil(data) + +@radius_event +def authorize_user(data): + nas = data.get('NAS-IP-Address', None) + nas_id = data.get('NAS-Identifier', None) + user = data.get('User-Name', None) + password = data.get('User-Password', None) + out = subprocess.check_output(['/usr/bin/python3', '/var/www/re2o/freeradius_utils/authenticate_user.py', user, password]) + if out[:-1] == u"TRUE": + if data.get('Service-Type', '')==u'NAS-Prompt-User': + logger.info(u"Access of user %s on %s (%s)" % (user, nas, nas_id)) + elif data.get('Service-Type', '')==u'Administrative-User': + logger.info(u"Enable manager for %s on %s (%s)" % (user, nas, nas_id)) + return (radiusd.RLM_MODULE_UPDATED, + (), + ( + ("Auth-Type", "Accept"), + ), + ) + else: + return (radiusd.RLM_MODULE_UPDATED, + (), + ( + ("Auth-Type", "Reject"), + ), + ) + + +@radius_event +def authorize_fil(data): + """ + Check le challenge chap, et accepte. + """ + + return (radiusd.RLM_MODULE_UPDATED, + (), + ( + ("Auth-Type", "Accept"), + ), + ) + +@radius_event +def post_auth(data): + # On cherche quel est le type de machine, et quel sites lui appliquer + if data.get('NAS-Port-Type', '')==u'Ethernet': + return post_auth_fil(data) + elif u"Wireless" in data.get('NAS-Port-Type', ''): + return post_auth_wifi(data) + +@radius_event +def post_auth_wifi(data): + """Appelé une fois que l'authentification est ok. + On peut rajouter quelques éléments dans la réponse radius ici. + Comme par exemple le vlan sur lequel placer le client""" + + port, vlan_name, reason = decide_vlan(data, True) + mac = data.get('Calling-Station-Id', None) + + log_message = '(wifi) %s -> %s [%s%s]' % \ + (port, mac, vlan_name, (reason and u': ' + reason).encode('utf-8')) + logger.info(log_message) + + # Si NAS ayant des mapping particuliers, à signaler ici + vlan_id = config.vlans[vlan_name] + + # WiFi : Pour l'instant, on ne met pas d'infos de vlans dans la réponse + # les bornes wifi ont du mal avec cela + if WIFI_DYN_VLAN: + return (radiusd.RLM_MODULE_UPDATED, + ( + ("Tunnel-Type", "VLAN"), + ("Tunnel-Medium-Type", "IEEE-802"), + ("Tunnel-Private-Group-Id", '%d' % vlan_id), + ), + () + ) + + return radiusd.RLM_MODULE_OK + +@radius_event +def post_auth_fil(data): + """Idem, mais en filaire. + """ + + nas = data.get('NAS-IP-Address', None) + port = data.get('NAS-Port', None) + mac = data.get('Calling-Station-Id', None) + # Hack, à cause d'une numérotation cisco baroque + port = port[-2:] + out = subprocess.check_output(['/usr/bin/python3', '/var/www/re2o/freeradius_utils/authenticate_filaire.py', nas, port, mac]) + sw_name, reason, vlan_id = make_tuple(out) + + log_message = '(fil) %s -> %s [%s%s]' % \ + (sw_name + u":" + port, mac, vlan_id, (reason and u': ' + reason).encode('utf-8')) + logger.info(log_message) + + # Filaire + return (radiusd.RLM_MODULE_UPDATED, + ( + ("Tunnel-Type", "VLAN"), + ("Tunnel-Medium-Type", "IEEE-802"), + ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)), + ), + () + ) + +@radius_event +def dummy_fun(_): + """Do nothing, successfully. (C'est pour avoir un truc à mettre)""" + return radiusd.RLM_MODULE_OK + +def detach(_=None): + """Appelé lors du déchargement du module (enfin, normalement)""" + print "*** goodbye from auth.py ***" + return radiusd.RLM_MODULE_OK + diff --git a/freeradius_utils/authenticate_filaire.py b/freeradius_utils/authenticate_filaire.py new file mode 100755 index 00000000..3640240a --- /dev/null +++ b/freeradius_utils/authenticate_filaire.py @@ -0,0 +1,82 @@ +import os, sys + +proj_path = "/var/www/re2o/" +# This is so Django knows where to find stuff. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") +sys.path.append(proj_path) + +# This is so my local_settings.py gets loaded. +os.chdir(proj_path) + +# This is so models get loaded. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +import argparse + +from machines.models import Interface, IpList +from topologie.models import Room, Port, Switch +from users.models import User + +from re2o.settings import RADIUS_VLAN_DECISION + +VLAN_NOK = RADIUS_VLAN_DECISION['VLAN_NOK'] +VLAN_OK = RADIUS_VLAN_DECISION['VLAN_OK'] + +def decide_vlan(switch_ip, port_number, mac_address): + # Get port from switch and port number + switch = Switch.objects.filter(switch_interface=Interface.objects.filter(ipv4=IpList.objects.filter(ipv4=switch_ip))) + if switch: + sw_name = str(switch[0].switch_interface) + port = Port.objects.filter(switch=switch[0], port=port_number) + if port: + port = port[0] + if port.radius == 'NO': + # Aucune authentification sur ce port + decision = (sw_name, "Pas d'authentification sur ce port", VLAN_OK) + elif port.radius == 'BLOQ': + # Prise désactivée + decision = (sw_name, 'Port desactive', VLAN_NOK) + elif port.radius == 'COMMON': + # Authentification par mac + interface = Interface.objects.filter(mac_address=mac_address) + if not interface: + decision = (sw_name, 'Mac not found', VLAN_NOK) + elif not interface[0].is_active(): + decision = (sw_name, 'Machine non active / adherent non cotisant', VLAN_NOK) + else: + decision = (sw_name, 'Machine OK', VLAN_OK) + elif port.radius == 'STRICT': + if port.room: + user = User.objects.filter(room=Room.objects.filter(name=port.room)) + if not user: + decision = (sw_name, 'Chambre non cotisante', VLAN_NOK) + elif not user[0].has_access(): + decision = (sw_name, 'Resident desactive', VLAN_NOK) + else: + # Verification de la mac + interface = Interface.objects.filter(mac_address=mac_address) + if not interface: + decision = (sw_name, 'Chambre Ok, but mac not found', VLAN_NOK) + elif not interface[0].is_active(): + decision = (sw_name, 'Chambre Ok, but machine non active / adherent non cotisant', VLAN_NOK) + else: + decision = (sw_name, 'Machine OK, Proprio OK', VLAN_OK) + else: + decision = (sw_name, 'Chambre inconnue', VLAN_NOK) + else: + decision = (sw_name, 'VLAN forced', int(port.radius)) + else: + decision = (sw_name, 'port not found!', VLAN_OK) + else: + decision = ('?', 'switch not found!', VLAN_OK) + return decision + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Decide radius vlan attribution') + parser.add_argument('switch_ip', action="store") + parser.add_argument('port_number', action="store", type=int) + parser.add_argument('mac_address', action="store") + args = parser.parse_args() + print(decide_vlan(args.switch_ip, args.port_number, args.mac_address)) + diff --git a/freeradius_utils/authenticate_user.py b/freeradius_utils/authenticate_user.py new file mode 100755 index 00000000..933ba9a3 --- /dev/null +++ b/freeradius_utils/authenticate_user.py @@ -0,0 +1,37 @@ +import os, sys + +proj_path = "/var/www/re2o/" +# This is so Django knows where to find stuff. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings") +sys.path.append(proj_path) + +# This is so my local_settings.py gets loaded. +os.chdir(proj_path) + +# This is so models get loaded. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +import argparse + +from django.contrib.auth import authenticate +from users.models import User + + +def authorize_user(user, password): + user = authenticate(username=user, password=password) + if user: + if User.objects.get(pseudo=user): + return "TRUE" + else: + return "FALSE" + else: + return "FALSE" + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Authorize user') + parser.add_argument('user', action="store") + parser.add_argument('password', action="store") + args = parser.parse_args() + print(authorize_user(args.user, args.password)) + diff --git a/freeradius_utils/modules/rlm_python_re2o.conf b/freeradius_utils/modules/rlm_python_re2o.conf new file mode 120000 index 00000000..5bd5d2ad --- /dev/null +++ b/freeradius_utils/modules/rlm_python_re2o.conf @@ -0,0 +1 @@ +../rlm_python_re2o.conf \ No newline at end of file