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

@ -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=<automatique>)(host=%s.wifi.crans.org))' %
(base, username),
u'(&%s(macAddress=<automatique>)(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 '<automatique>' 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