From e45546afdbbfa6123581bdd94a5dc65e371e97e8 Mon Sep 17 00:00:00 2001 From: Daniel STAN Date: Tue, 22 Apr 2014 12:47:20 +0200 Subject: [PATCH] =?UTF-8?q?freeradius/auth.py=20:=20impl=C3=A9mente=20auth?= =?UTF-8?q?=20filaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freeradius/auth.py | 259 +++++++++++++++++++++++++-------------------- 1 file changed, 145 insertions(+), 114 deletions(-) diff --git a/freeradius/auth.py b/freeradius/auth.py index a4f23a2f..d602e3db 100644 --- a/freeradius/auth.py +++ b/freeradius/auth.py @@ -6,19 +6,7 @@ # Ce fichier contient la définition de plusieurs fonctions d'interface à freeradius # qui peuvent être appelées (suivant les configurations) à certains moment de # l'éxécution. -# -# 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 -# (access ok et autres trucs du genre) -# * un tuple de couples (clé, valeur) pour les valeurs internes à mettre à jour -# (mot de passe par exemple) # -# Voir des exemples plus complets ici: -# https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ - import lc_ldap.shortcuts from lc_ldap.crans_utils import escape as escape_ldap import lc_ldap.crans_utils @@ -28,24 +16,16 @@ import radiusd import netaddr import traceback from gestion.gen_confs.trigger import trigger_generate_cochon as trigger_generate +import annuaires_pg # Voilà, pour faire marcher le V6Only, il faut se retirer l'ipv4 de sa machine # ou en enregistrer une nouvelle (sans ipv4) avec une autre mac. Moi j'ai la # flemme donc je hardcode les MAC qui doivent toujours être placées sur le vlan v6only test_v6 = [ -u'dc:9f:db:5c:c3:ea', # polynice-wlan0 -#u'00:15:6d:ee:e8:f8', # atree-wlan0 -u'00:26:c7:a6:9e:16', # cerveaulent + u'00:26:c7:a6:9e:16', # cerveaulent (machine de b2moo) ] -# TODO (à metre dans bcfg2) -#setfacl -m u:freerad:rx /etc/crans/ -#setfacl -m u:freerad:rx /etc/crans/secrets -#setfacl -m u:freerad:r /etc/crans/secrets/dhcp.py -#setfacl -m u:freerad:r /etc/crans/secrets/secrets.py -#setfacl -m u:freerad:r /etc/crans/secrets/trigger-generate.pub -#setfacl -m m::r /etc/crans/secrets/trigger-generate -#setfacl -m u:freerad:r /etc/crans/secrets/trigger-generate +USERNAME_SUFFIX = '.wifi.crans.org' bl_reject = [u'bloq'] bl_isolement = [u'virus', u'autodisc_virus', u'autodisc_p2p', u'ipv6_ra'] @@ -59,41 +39,65 @@ use_ldap_admin = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, use_ldap = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, constructor=lc_ldap.shortcuts.lc_ldap_anonymous) -@use_ldap -def get_machines(auth_data, conn): - mac = None - username = None - - # Beware: les valeurs scalaires sont entre guillemets - # Calling-Station-Id: "une_adresse_mac" - for (key, value) in auth_data: - if key == 'Calling-Station-Id': - try: - value = value.decode('ascii', 'ignore').replace('"','') - mac = lc_ldap.crans_utils.format_mac(value) - except: - radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !') - if key == 'User-Name': - username = escape_ldap(value.decode('ascii', 'ignore').replace('"','')) +def radius_event(f): + """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) + Voir des exemples plus complets ici: + https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/ - # TODO - # format username (strip .wifi.crans.org) + 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): + 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('"', '') + return f(data) + + return new_f + +@use_ldap +def get_machines(data, conn): + """Obtient la liste de machine essayant actuellement de se connecter""" + mac = data.get('Calling-Station-Id', None) + if mac: + try: + mac = lc_ldap.crans_utils.format_mac(mac.decode('ascii', 'ignore')) + except: + radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !') + mac = None + username = data.get('User-Name', None) + if username: + username = escape_ldap(username.decode('ascii', 'ignore')) + if username.endswith(USERNAME_SUFFIX): + username = username[:-len(USERNAME_SUFFIX)] base = u'(objectclass=machine)' if mac is None: radiusd.radlog(radiusd.L_ERR, 'Cannot read client MAC from AP !') - return [] - + if username is None: + radiusd.radlog(radiusd.L_ERR, 'Cannot read client User-Name !') + + # Sanitize all the things mac = escape_ldap(mac) username = escape_ldap(username) + # Liste de filtres ldap à essayer search_strats = [ # Case 1: Search by mac (reported by AP) u'(&%s(macAddress=%s))' % (base, mac), # Case 2: unregistered mac - u'(&%s(macAddress=)(host=%s.wifi.crans.org))' % - (base, username), + u'(&%s(macAddress=)(host=%s%s))' % + (base, username, USERNAME_SUFFIX), ] for filter_s in search_strats: @@ -103,50 +107,65 @@ def get_machines(auth_data, conn): return res -def get_prise(auth_data): - """Extrait la prise""" - ## Regarder dans +def get_prise_chbre(data): + """Extrait la prise (filaire) et la chambre correspondante. + Par convention, le nom d'une chambre commence (lettre du bâtiment) par une + majuscule, tandis que la prise correspondante commence par une miniscule. + """ ## Filaire: NAS-Identifier => contient le nom du switch (batm-3.adm.crans.org) ## Nas-Port => port du switch (ex 42) + + # Lettre du bâtiment (C, B, A, etc, en majuscule) + bat_name = None + + # Numéro du switch + bat_num = None + + # Port sur le switch + port = None + + nas = data.get('Nas-Identifier', None) + if nas: + if nas.startswith('bat'): + nas = value.split('.', 1)[0] + try: + bat_name = nas[3].upper() + bat_num = int(nas.split('-', 1)[1]) + except IndexError, ValueError: + pass + port = data.get('Nas-Port', None) + if port: + port = int(value) + + if bat_num is not None and bat_name and port: + prise = bat_name.lower() + "%01d%02d" % (bat_num, port) + try: + chbre = bat_name + annuaires_pg.reverse(bat_name, prise[1:])[0] + except IndexError: + chbre = None + return prise, chbre + +def get_ap(data): + """Extrait la prise (wifi)""" ## WiFi: NAS-Identifier => vide ## Nas-Port => numéro sur l'interface ## Nas-IP-Address => adresse IP de la borne - is_wifi = True - bat_name = None - bat_num = None - port = None - - for (key, value) in auth_data: - if key == 'NAS-Identifier': - if value.startswith('bat'): - nas = value.split('.', 1)[0] - try: - bat_name = nas[3] - bat_num = nas.split('-', 1)[1] - except IndexError: - pass - - if key == 'Nas-Port': - port = int(value) - - if bat_num and bat_name and port: - return bat_name + "%01d%02d" % (bat_num, port) - + pass @use_ldap_admin -def register_mac(auth_data, machine, conn): - for (key, value) in auth_data: - if key == 'Calling-Station-Id': - try: - value = value.decode('ascii', 'ignore').replace('"','') - mac = lc_ldap.crans_utils.format_mac(value) - except: - radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !') - +def register_mac(data, machine, conn): + """Enregistre la mac actuelle sur une machine donnée.""" + mac = data.get('Calling-Station-Id', None) if mac is None: radiusd.radlog(radiusd.L_ERR, 'Cannot find MAC') return - mac = unicode(mac.lower()) + mac = mac.decode('ascii', 'ignore').replace('"','') + try: + mac = lc_ldap.crans_utils.format_mac(value) + except: + radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !') + return + with conn.search(unicode(machine.dn.split(',',1)[0]), mode='rw')[0] as machine: radiusd.radlog(radiusd.L_INFO, 'Registering mac %s' % mac) machine['macAddress'] = mac @@ -158,6 +177,7 @@ def register_mac(auth_data, machine, conn): trigger_generate('komaz') radiusd.radlog(radiusd.L_INFO, 'done ! (triggered komaz)') +@radius_event @use_ldap_admin @use_ldap def instantiate(p, *conns): @@ -165,15 +185,16 @@ def instantiate(p, *conns): do nothing)""" pass +@radius_event @use_ldap -def wifi_authorize(auth_data, conn): +def wifi_authorize(data, conn): """Section authorize pour le wifi (NB: le filaire est en accept pour tout le monde) Éxécuté avant l'authentification proprement dite. On peut ainsi remplir les champs login et mot de passe qui serviront ensuite à l'authentification (MschapV2/PEAP ou MschapV2/TTLS)""" - - items = get_machines(auth_data) + + items = get_machines(data) if not items: radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Nobody found') @@ -185,18 +206,17 @@ def wifi_authorize(auth_data, conn): machine = items[0] if '' in machine['macAddress']: - register_mac(auth_data, machine) - - + register_mac(data, machine) + proprio = machine.proprio() if isinstance(proprio, lc_ldap.objets.AssociationCrans): - radiusd.radlog(radiusd.L_ERR, 'Crans machine trying to authenticate !') + radiusd.radlog(radiusd.L_ERR, 'Crans machine trying to authenticate !') return radiusd.RLM_MODULE_INVALID for bl in machine.blacklist_actif(): if bl.value['type'] in bl_reject: return radiusd.RLM_MODULE_REJECT - + if not machine.get('ipsec', False): radiusd.radlog(radiusd.L_ERR, 'WiFi authentication but machine has no' + 'password') @@ -212,19 +232,22 @@ def wifi_authorize(auth_data, conn): ), ) +@radius_event @use_ldap -def post_auth(auth_data, conn): +def post_auth(data, conn): """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""" + is_wifi = False vlan_name = None reason = '' identity = "" #TODO - prise = "" #TODO - items = get_machines(auth_data) - + prise = "" + chbre = None + items = get_machines(data) decision = 'adherent','' + if not items: decision = 'accueil', 'Machine inconnue' @@ -233,7 +256,8 @@ def post_auth(auth_data, conn): if isinstance(machine, lc_ldap.objets.machineWifi): decision = 'wifi', '' - + is_wifi = True + if not machine['ipHostNumber'] or unicode(machine['macAddress'][0]) in test_v6: decision = 'v6only', 'No IPv4' elif machine['ipHostNumber'][0].value in netaddr.IPNetwork('10.2.9.0/24'): @@ -246,35 +270,41 @@ def post_auth(auth_data, conn): if bl.value['type'] in bl_accueil: decision = 'accueil', unicode(bl) + # Filaire : protection anti-"squattage" + if not is_wifi: + # Si l'adhérent n'est pas membre actif, il doit se brancher depuis la + # prise d'un autre adhérent à jour de cotisation + prise, chbre = get_prise_chbre(data) + if proprio['droits']: + decision = decision[0], decision[1] + ' (force MA)' + elif chbre is None: + decision = "accueil", "Chambre inconnue" + else: + chbre = escape_ldap(chbre) + hebergeurs = conn.search(u'(&(chambre=%s)(cid=*)(aid=*))' % chbre) + for hebergeur in hebergeurs: + if not hebergeur.blacklist_actif(): + break + else: + decision = "accueil", "Hébergeur blacklisté" + + # Unpack and log + if chbre is not None: + prise += '/' + chbre + vlan_name, reason = decision vlan = vlans[vlan_name] - radiusd.radlog(radiusd.L_INFO, 'auth.py: %s -> %s [%s%s]' % + radiusd.radlog(radiusd.L_INFO, 'auth.py: %s -> %s [%s%s]' % (prise, identity, vlan_name, (reason and u': ' + reason).encode(u'utf-8')) ) -# -# -# # Si l'adhérent n'est pas membre actif, il doit se brancher depuis la prise -# # d'un autre adhérent à jour de cotisation -# if not proprio.droits(): -# try: -# chbre = prise[0] + annuaires_pg.reverse(prise[0], prise[1:])[0] -# except IndexError: -# return (0, "Chambre inconnue", "accueil") -# hebergeurs = conn.search('chambre=' + chbre) -# for hebergeur in hebergeurs['adherent'] + hebergeurs['club']: -# if paiement_ok(hebergeur): -# break -# else: -# return (0, "Hébergeur non à jour", "accueil") -# -# - # Pour l'instant, on ne met pas d'infos de vlans dans la réponse - return radiusd.RLM_MODULE_OK + # 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 is_wifi: + return radiusd.RLM_MODULE_OK - # This is dead code (for now) return (radiusd.RLM_MODULE_UPDATED, - ( + ( ("Tunnel-Type", "VLAN"), ("Tunnel-Medium-Type", "IEEE-802"), ("Tunnel-Private-Group-Id", '%d' % vlan), @@ -282,6 +312,7 @@ def post_auth(auth_data, conn): () ) +@radius_event def dummy_fun(p): return radiusd.RLM_MODULE_OK