#!/bin/bash /usr/scripts/python.sh # ⁻*- coding: utf-8 -*- # # Draft de fichier d'authentification # # 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. # import lc_ldap.shortcuts from lc_ldap.crans_utils import escape as escape_ldap import lc_ldap.crans_utils from gestion.config.config import vlans import lc_ldap.objets 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'00:26:c7:a6:9e:16', # cerveaulent (machine de b2moo) ] USERNAME_SUFFIX = '.wifi.crans.org' bl_reject = [u'bloq'] bl_isolement = [u'virus', u'autodisc_virus', u'autodisc_p2p', u'ipv6_ra'] # TODO carte_etudiant: dépend si sursis ou non (regarder lc_ldap) # TODO LOGSSSSS bl_accueil = [u'carte_etudiant', u'chambre_invalide', u'paiement'] # Decorateur utilisé plus tard (same connection) use_ldap_admin = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, constructor=lc_ldap.shortcuts.lc_ldap_admin) use_ldap = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, constructor=lc_ldap.shortcuts.lc_ldap_anonymous) 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/ 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 !') 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%s))' % (base, username, USERNAME_SUFFIX), ] for filter_s in search_strats: res = conn.search(filter_s) if res: break return res 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 pass @use_ldap_admin 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 = 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 machine.history_add(u'auth.py', u'macAddress ( -> %s)' % mac) machine.validate_changes() machine.save() radiusd.radlog(radiusd.L_INFO, 'Mac set') radiusd.radlog(radiusd.L_INFO, 'Triggering komaz') trigger_generate('komaz') radiusd.radlog(radiusd.L_INFO, 'done ! (triggered komaz)') @radius_event @use_ldap_admin @use_ldap def instantiate(p, *conns): """Utile pour initialiser les connexions ldap une première fois (otherwise, do nothing)""" pass @radius_event @use_ldap 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(data) if not items: radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Nobody found') return radiusd.RLM_MODULE_NOTFOUND if len(items) > 1: radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Too many results (took first)') machine = items[0] if '' in machine['macAddress']: register_mac(data, machine) proprio = machine.proprio() if isinstance(proprio, lc_ldap.objets.AssociationCrans): 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') return radiusd.RLM_MODULE_REJECT password = machine['ipsec'][0].value.encode('ascii', 'ignore') # TODO: feed cert here return (radiusd.RLM_MODULE_UPDATED, (), ( ("Cleartext-Password", password), ), ) @radius_event @use_ldap 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 = "" chbre = None items = get_machines(data) decision = 'adherent','' if not items: decision = 'accueil', 'Machine inconnue' machine = items[0] proprio = machine.proprio() 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'): # Cas des personnels logés dans les appartements de l'ENS decision = 'appts', 'Personnel ENS' for bl in machine.blacklist_actif(): if bl.value['type'] in bl_isolement: decision = 'isolement', unicode(bl) 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]' % (prise, identity, vlan_name, (reason and u': ' + reason).encode(u'utf-8')) ) # 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 return (radiusd.RLM_MODULE_UPDATED, ( ("Tunnel-Type", "VLAN"), ("Tunnel-Medium-Type", "IEEE-802"), ("Tunnel-Private-Group-Id", '%d' % vlan), ), () ) @radius_event def dummy_fun(p): return radiusd.RLM_MODULE_OK def detach(p=None): """Appelé lors du déchargement du module (enfin, normalement)""" print "*** goodbye from example.py ***" return radiusd.RLM_MODULE_OK