scripts/freeradius/auth.py
2014-07-08 14:39:42 +02:00

323 lines
11 KiB
Python

#!/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=<automatique>)(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(mac).lower()
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 (<automatique> -> %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 '<automatique>' 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'
return radiusd.RLM_MODULE_NOTFOUND # TODO faire un truc plus propre
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