freeradius/auth.py : implémente auth filaire

This commit is contained in:
Daniel STAN 2014-04-22 12:47:20 +02:00
parent 366814f8de
commit e45546afdb

View file

@ -7,18 +7,6 @@
# qui peuvent être appelées (suivant les configurations) à certains moment de # qui peuvent être appelées (suivant les configurations) à certains moment de
# l'éxécution. # 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 import lc_ldap.shortcuts
from lc_ldap.crans_utils import escape as escape_ldap from lc_ldap.crans_utils import escape as escape_ldap
import lc_ldap.crans_utils import lc_ldap.crans_utils
@ -28,24 +16,16 @@ import radiusd
import netaddr import netaddr
import traceback import traceback
from gestion.gen_confs.trigger import trigger_generate_cochon as trigger_generate 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 # 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 # 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 # flemme donc je hardcode les MAC qui doivent toujours être placées sur le vlan v6only
test_v6 = [ test_v6 = [
u'dc:9f:db:5c:c3:ea', # polynice-wlan0 u'00:26:c7:a6:9e:16', # cerveaulent (machine de b2moo)
#u'00:15:6d:ee:e8:f8', # atree-wlan0
u'00:26:c7:a6:9e:16', # cerveaulent
] ]
# TODO (à metre dans bcfg2) USERNAME_SUFFIX = '.wifi.crans.org'
#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
bl_reject = [u'bloq'] bl_reject = [u'bloq']
bl_isolement = [u'virus', u'autodisc_virus', u'autodisc_p2p', u'ipv6_ra'] 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, use_ldap = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5,
constructor=lc_ldap.shortcuts.lc_ldap_anonymous) 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 @use_ldap
def get_machines(auth_data, conn): def get_machines(data, conn):
mac = None """Obtient la liste de machine essayant actuellement de se connecter"""
username = None mac = data.get('Calling-Station-Id', None)
if mac:
# Beware: les valeurs scalaires sont entre guillemets try:
# Calling-Station-Id: "une_adresse_mac" mac = lc_ldap.crans_utils.format_mac(mac.decode('ascii', 'ignore'))
for (key, value) in auth_data: except:
if key == 'Calling-Station-Id': radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !')
try: mac = None
value = value.decode('ascii', 'ignore').replace('"','') username = data.get('User-Name', None)
mac = lc_ldap.crans_utils.format_mac(value) if username:
except: username = escape_ldap(username.decode('ascii', 'ignore'))
radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !') if username.endswith(USERNAME_SUFFIX):
if key == 'User-Name': username = username[:-len(USERNAME_SUFFIX)]
username = escape_ldap(value.decode('ascii', 'ignore').replace('"',''))
# TODO
# format username (strip .wifi.crans.org)
base = u'(objectclass=machine)' base = u'(objectclass=machine)'
if mac is None: if mac is None:
radiusd.radlog(radiusd.L_ERR, 'Cannot read client MAC from AP !') 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) mac = escape_ldap(mac)
username = escape_ldap(username) username = escape_ldap(username)
# Liste de filtres ldap à essayer
search_strats = [ search_strats = [
# Case 1: Search by mac (reported by AP) # Case 1: Search by mac (reported by AP)
u'(&%s(macAddress=%s))' % (base, mac), u'(&%s(macAddress=%s))' % (base, mac),
# Case 2: unregistered mac # Case 2: unregistered mac
u'(&%s(macAddress=<automatique>)(host=%s.wifi.crans.org))' % u'(&%s(macAddress=<automatique>)(host=%s%s))' %
(base, username), (base, username, USERNAME_SUFFIX),
] ]
for filter_s in search_strats: for filter_s in search_strats:
@ -103,50 +107,65 @@ def get_machines(auth_data, conn):
return res return res
def get_prise(auth_data): def get_prise_chbre(data):
"""Extrait la prise""" """Extrait la prise (filaire) et la chambre correspondante.
## Regarder dans 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) ## Filaire: NAS-Identifier => contient le nom du switch (batm-3.adm.crans.org)
## Nas-Port => port du switch (ex 42) ## 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 ## WiFi: NAS-Identifier => vide
## Nas-Port => numéro sur l'interface ## Nas-Port => numéro sur l'interface
## Nas-IP-Address => adresse IP de la borne ## Nas-IP-Address => adresse IP de la borne
is_wifi = True pass
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)
@use_ldap_admin @use_ldap_admin
def register_mac(auth_data, machine, conn): def register_mac(data, machine, conn):
for (key, value) in auth_data: """Enregistre la mac actuelle sur une machine donnée."""
if key == 'Calling-Station-Id': mac = data.get('Calling-Station-Id', None)
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 mac is None: if mac is None:
radiusd.radlog(radiusd.L_ERR, 'Cannot find MAC') radiusd.radlog(radiusd.L_ERR, 'Cannot find MAC')
return 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: with conn.search(unicode(machine.dn.split(',',1)[0]), mode='rw')[0] as machine:
radiusd.radlog(radiusd.L_INFO, 'Registering mac %s' % mac) radiusd.radlog(radiusd.L_INFO, 'Registering mac %s' % mac)
machine['macAddress'] = mac machine['macAddress'] = mac
@ -158,6 +177,7 @@ def register_mac(auth_data, machine, conn):
trigger_generate('komaz') trigger_generate('komaz')
radiusd.radlog(radiusd.L_INFO, 'done ! (triggered komaz)') radiusd.radlog(radiusd.L_INFO, 'done ! (triggered komaz)')
@radius_event
@use_ldap_admin @use_ldap_admin
@use_ldap @use_ldap
def instantiate(p, *conns): def instantiate(p, *conns):
@ -165,15 +185,16 @@ def instantiate(p, *conns):
do nothing)""" do nothing)"""
pass pass
@radius_event
@use_ldap @use_ldap
def wifi_authorize(auth_data, conn): def wifi_authorize(data, conn):
"""Section authorize pour le wifi """Section authorize pour le wifi
(NB: le filaire est en accept pour tout le monde) (NB: le filaire est en accept pour tout le monde)
Éxécuté avant l'authentification proprement dite. On peut ainsi remplir les Éxécuté avant l'authentification proprement dite. On peut ainsi remplir les
champs login et mot de passe qui serviront ensuite à l'authentification champs login et mot de passe qui serviront ensuite à l'authentification
(MschapV2/PEAP ou MschapV2/TTLS)""" (MschapV2/PEAP ou MschapV2/TTLS)"""
items = get_machines(auth_data) items = get_machines(data)
if not items: if not items:
radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Nobody found') radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Nobody found')
@ -185,8 +206,7 @@ def wifi_authorize(auth_data, conn):
machine = items[0] machine = items[0]
if '<automatique>' in machine['macAddress']: if '<automatique>' in machine['macAddress']:
register_mac(auth_data, machine) register_mac(data, machine)
proprio = machine.proprio() proprio = machine.proprio()
if isinstance(proprio, lc_ldap.objets.AssociationCrans): if isinstance(proprio, lc_ldap.objets.AssociationCrans):
@ -212,19 +232,22 @@ def wifi_authorize(auth_data, conn):
), ),
) )
@radius_event
@use_ldap @use_ldap
def post_auth(auth_data, conn): def post_auth(data, conn):
"""Appelé une fois que l'authentification est ok. """Appelé une fois que l'authentification est ok.
On peut rajouter quelques éléments dans la réponse radius ici. On peut rajouter quelques éléments dans la réponse radius ici.
Comme par exemple le vlan sur lequel placer le client""" Comme par exemple le vlan sur lequel placer le client"""
is_wifi = False
vlan_name = None vlan_name = None
reason = '' reason = ''
identity = "" #TODO identity = "" #TODO
prise = "" #TODO prise = ""
items = get_machines(auth_data) chbre = None
items = get_machines(data)
decision = 'adherent','' decision = 'adherent',''
if not items: if not items:
decision = 'accueil', 'Machine inconnue' decision = 'accueil', 'Machine inconnue'
@ -233,6 +256,7 @@ def post_auth(auth_data, conn):
if isinstance(machine, lc_ldap.objets.machineWifi): if isinstance(machine, lc_ldap.objets.machineWifi):
decision = 'wifi', '' decision = 'wifi', ''
is_wifi = True
if not machine['ipHostNumber'] or unicode(machine['macAddress'][0]) in test_v6: if not machine['ipHostNumber'] or unicode(machine['macAddress'][0]) in test_v6:
decision = 'v6only', 'No IPv4' decision = 'v6only', 'No IPv4'
@ -246,33 +270,39 @@ def post_auth(auth_data, conn):
if bl.value['type'] in bl_accueil: if bl.value['type'] in bl_accueil:
decision = 'accueil', unicode(bl) 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_name, reason = decision
vlan = vlans[vlan_name] 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')) (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
# # Si l'adhérent n'est pas membre actif, il doit se brancher depuis la prise if is_wifi:
# # d'un autre adhérent à jour de cotisation return radiusd.RLM_MODULE_OK
# 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
# This is dead code (for now)
return (radiusd.RLM_MODULE_UPDATED, return (radiusd.RLM_MODULE_UPDATED,
( (
("Tunnel-Type", "VLAN"), ("Tunnel-Type", "VLAN"),
@ -282,6 +312,7 @@ def post_auth(auth_data, conn):
() ()
) )
@radius_event
def dummy_fun(p): def dummy_fun(p):
return radiusd.RLM_MODULE_OK return radiusd.RLM_MODULE_OK