auth.py 14.7 KB
Newer Older
1
# ⁻*- mode: python; coding: utf-8 -*-
2 3 4 5
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
6
# Copyirght © 2017  Daniel Stan
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Gabriel Detraz's avatar
Gabriel Detraz committed
25 26 27 28 29 30 31 32 33
"""
Backend python pour freeradius.

Ce fichier contient la définition de plusieurs fonctions d'interface à
freeradius qui peuvent être appelées (suivant les configurations) à certains
moment de l'authentification, en WiFi, filaire, ou par les NAS eux-mêmes.

Inspirés d'autres exemples trouvés ici :
https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/
34 35

Inspiré du travail de Daniel Stan au Crans
Gabriel Detraz's avatar
Gabriel Detraz committed
36 37 38 39 40 41 42 43
"""

import logging
import netaddr
import radiusd # Module magique freeradius (radiusd.py is dummy)
import binascii
import hashlib
import os, sys
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60


proj_path = "/var/www/re2o/"
# This is so Django knows where to find stuff.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings")
sys.path.append(proj_path)

# This is so my local_settings.py gets loaded.
os.chdir(proj_path)

# This is so models get loaded.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

import argparse

from django.db.models import Q
61
from machines.models import Interface, IpList, Nas, Domain
62 63 64 65 66 67 68 69
from topologie.models import Room, Port, Switch
from users.models import User
from preferences.models import OptionalTopologie

options, created = OptionalTopologie.objects.get_or_create()
VLAN_NOK = options.vlan_decision_nok.vlan_id
VLAN_OK = options.vlan_decision_ok.vlan_id

Gabriel Detraz's avatar
Gabriel Detraz committed
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129

#: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))


## -*- Logging -*-

class RadiusdHandler(logging.Handler):
    """Handler de logs pour freeradius"""

    def emit(self, record):
        """Process un message de log, en convertissant les niveaux"""
        if record.levelno >= logging.WARN:
            rad_sig = radiusd.L_ERR
        elif record.levelno >= logging.INFO:
            rad_sig = radiusd.L_INFO
        else:
            rad_sig = radiusd.L_DBG
        radiusd.radlog(rad_sig, record.msg)

# Initialisation d'un logger (pour logguer unifié)
logger = logging.getLogger('auth.py')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s')
handler = RadiusdHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)

def radius_event(fun):
    """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)

    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):
        if type(auth_data) == dict:
            data = auth_data
        else:
            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('"', '')
        try:
            # TODO s'assurer ici que les tuples renvoyés sont bien des (str,str)
            # rlm_python ne digère PAS les unicodes
            return fun(data)
        except Exception as err:
            logger.error('Failed %r on data %r' % (err, auth_data))
            raise

    return new_f

130

Gabriel Detraz's avatar
Gabriel Detraz committed
131 132 133 134 135 136 137

@radius_event
def instantiate(*_):
    """Utile pour initialiser les connexions ldap une première fois (otherwise,
    do nothing)"""
    logger.info('Instantiation')
    if TEST_SERVER:
138
        logger.info(u'DBG_FREERADIUS is enabled')
Gabriel Detraz's avatar
Gabriel Detraz committed
139 140 141

@radius_event
def authorize(data):
142 143 144 145 146
    """On test si on connait le calling nas:
    - si le nas est inconnue, on suppose que c'est une requète 802.1X, on la traite
    - si le nas est connu, on applique 802.1X si le mode est activé
    - si le nas est connu et si il s'agit d'un nas auth par mac, on repond accept en authorize
    """
147
    # Pour les requetes proxifiees, on split
148 149 150
    nas = data.get('NAS-IP-Address', data.get('NAS-Identifier', None))
    nas_instance = find_nas_from_request(nas)
    # Toutes les reuquètes non proxifiées
Gabriel Detraz's avatar
Gabriel Detraz committed
151
    nas_type = None
152
    if nas_instance:
153 154
        nas_type = Nas.objects.filter(nas_type=nas_instance.type).first()
    if not nas_type or nas_type.port_access_mode == '802.1X':
155
        user = data.get('User-Name', '').decode('utf-8', errors='replace')
156
        user = user.split('@', 1)[0]
157
        mac = data.get('Calling-Station-Id', '')
158
        result, log, password = check_user_machine_and_register(nas_type, user, mac)
159
        logger.info(log.encode('utf-8'))
160 161
        logger.info(user.encode('utf-8'))

162 163 164 165 166 167 168 169 170
        if not result:
            return radiusd.RLM_MODULE_REJECT
        else:
            return (radiusd.RLM_MODULE_UPDATED,
            (),
            (
            (str("NT-Password"), str(password)),
            ),
            )
Gabriel Detraz's avatar
Gabriel Detraz committed
171

172 173
    else:
        return (radiusd.RLM_MODULE_UPDATED,
Gabriel Detraz's avatar
Gabriel Detraz committed
174 175 176 177
	(),
        (
          ("Auth-Type", "Accept"),
        ),
178
        )
Gabriel Detraz's avatar
Gabriel Detraz committed
179 180 181

@radius_event
def post_auth(data):
182
    nas = data.get('NAS-IP-Address', data.get('NAS-Identifier', None))
root's avatar
root committed
183
    nas_instance = find_nas_from_request(nas)
184
    # Toutes les reuquètes non proxifiées
185 186
    if not nas_instance:
        logger.info(u"Requète proxifiée, nas inconnu".encode('utf-8'))
187 188 189 190 191
        return radiusd.RLM_MODULE_OK
    nas_type = Nas.objects.filter(nas_type=nas_instance.type).first()
    if not nas_type:
        logger.info(u"Type de nas non enregistré dans la bdd!".encode('utf-8'))
        return radiusd.RLM_MODULE_OK
192

Gabriel Detraz's avatar
Gabriel Detraz committed
193
    mac = data.get('Calling-Station-Id', None)
194

195 196
    # Switch et bornes héritent de machine et peuvent avoir plusieurs interfaces filles
    nas_machine = nas_instance.machine
197
    # Si il s'agit d'un switch
198
    if hasattr(nas_machine, 'switch'):
199
        port = data.get('NAS-Port-Id', data.get('NAS-Port', None))
200 201
        #Pour les infrastructures possédant des switchs Juniper :
        #On vérifie si le switch fait partie d'un stack Juniper
202
        instance_stack = nas_machine.switch.stack
203 204 205
        if instance_stack:
            # Si c'est le cas, on resélectionne le bon switch dans la stack
            id_stack_member = port.split("-")[1].split('/')[0]
206
            nas_machine = Switch.objects.filter(stack=instance_stack).filter(stack_member_id=id_stack_member).prefetch_related('interface_set__domain__extension').first()
207
        # On récupère le numéro du port sur l'output de freeradius. La ligne suivante fonctionne pour cisco, HP et Juniper
208
        port = port.split(".")[0].split('/')[-1][-2:]
209
        out = decide_vlan_and_register_switch(nas_machine, nas_type, port, mac)
210
        sw_name, room, reason, vlan_id = out
211 212

        log_message = '(fil) %s -> %s [%s%s]' % \
213
          (sw_name + u":" + port + u"/" + unicode(room), mac, vlan_id, (reason and u': ' + reason).encode('utf-8'))
214 215 216
        logger.info(log_message)

        # Filaire
Gabriel Detraz's avatar
Gabriel Detraz committed
217 218 219 220
        return (radiusd.RLM_MODULE_UPDATED,
            (
                ("Tunnel-Type", "VLAN"),
                ("Tunnel-Medium-Type", "IEEE-802"),
221
                ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)),
Gabriel Detraz's avatar
Gabriel Detraz committed
222 223
            ),
            ()
224
            )
Gabriel Detraz's avatar
Gabriel Detraz committed
225

226 227
    else:
        return radiusd.RLM_MODULE_OK
Gabriel Detraz's avatar
Gabriel Detraz committed
228 229 230 231 232 233 234 235 236 237 238

@radius_event
def dummy_fun(_):
    """Do nothing, successfully. (C'est pour avoir un truc à mettre)"""
    return radiusd.RLM_MODULE_OK

def detach(_=None):
    """Appelé lors du déchargement du module (enfin, normalement)"""
    print "*** goodbye from auth.py ***"
    return radiusd.RLM_MODULE_OK

239
def find_nas_from_request(nas_id):
chirac's avatar
chirac committed
240
    nas = Interface.objects.filter(Q(domain=Domain.objects.filter(name=nas_id)) | Q(ipv4=IpList.objects.filter(ipv4=nas_id))).select_related('type').select_related('machine__switch__stack')
root's avatar
root committed
241
    return nas.first()
242

243
def check_user_machine_and_register(nas_type, username, mac_address):
244 245 246 247 248 249
    """ Verifie le username et la mac renseignee. L'enregistre si elle est inconnue.
    Renvoie le mot de passe ntlm de l'user si tout est ok
    Utilise pour les authentifications en 802.1X"""
    interface = Interface.objects.filter(mac_address=mac_address).first()
    user = User.objects.filter(pseudo=username).first()
    if not user:
250 251 252
        return (False, u"User inconnu", '')
    if not user.has_access():
        return (False, u"Adhérent non cotisant", '')
253 254 255 256 257
    if interface:
        if interface.machine.user != user:
            return (False, u"Machine enregistrée sur le compte d'un autre user...", '')
        elif not interface.is_active:
            return (False, u"Machine desactivée", '')
258 259 260
        elif not interface.ipv4:
            interface.assign_ipv4()
            return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm)
261
        else:
262
            return (True, u"Access ok", user.pwd_ntlm)
263
    elif nas_type:
264
        if nas_type.autocapture_mac:
265 266 267 268
            result, reason = user.autoregister_machine(mac_address, nas_type)
            if result:
                return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm)
            else:
269
                return (False, u'Erreur dans le register mac %s' % reason, '')
270 271
        else:
            return (False, u'Machine inconnue', '')
272
    else:
273
        return (False, u"Machine inconnue", '')
274 275


276
def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, mac_address):
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
    """Fonction de placement vlan pour un switch en radius filaire auth par mac.
    Plusieurs modes :
    - nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK
    - pas de radius sur le port : VLAN_OK
    - bloq : VLAN_NOK
    - force : placement sur le vlan indiqué dans la bdd
    - mode strict :
        - pas de chambre associée : VLAN_NOK
        - pas d'utilisateur dans la chambre : VLAN_NOK
        - cotisation non à jour : VLAN_NOK
        - sinon passe à common (ci-dessous)
    - mode common :
        - interface connue (macaddress):
            - utilisateur proprio non cotisant ou banni : VLAN_NOK
            - user à jour : VLAN_OK
        - interface inconnue :
            - register mac désactivé : VLAN_NOK
            - register mac activé :
                - dans la chambre associé au port, pas d'user ou non à jour : VLAN_NOK
                - user à jour, autocapture de la mac et VLAN_OK
    """
298
    # Get port from switch and port number
299
    extra_log = ""
300
    # Si le NAS est inconnu, on place sur le vlan defaut
301
    if not nas_machine:
302
        return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK)
303

304
    sw_name = str(nas_machine)
305

306
    port = Port.objects.filter(switch=Switch.objects.filter(machine_ptr=nas_machine), port=port_number).first()
307
    #Si le port est inconnu, on place sur le vlan defaut
308
    if not port:
309
        return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK)
310

311 312
    # Si un vlan a été précisé, on l'utilise pour VLAN_OK
    if port.vlan_force:
313 314 315 316
        DECISION_VLAN = int(port.vlan_force.vlan_id)
        extra_log = u"Force sur vlan " + str(DECISION_VLAN)
    else:
        DECISION_VLAN = VLAN_OK
317 318

    if port.radius == 'NO':
319
        return (sw_name, "", u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN)
320 321

    if port.radius == 'BLOQ':
322
        return (sw_name, port.room, u'Port desactive', VLAN_NOK)
323 324

    if port.radius == 'STRICT':
325 326 327
        room = port.room
        if not room:
            return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK)
328

329
        room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room))
330
        if not room_user:
331
            return (sw_name, room, u'Chambre non cotisante', VLAN_NOK)
332 333
        for user in room_user:
            if not user.has_access():
334
                return (sw_name, room, u'Chambre resident desactive', VLAN_NOK)
335 336 337 338
        # else: user OK, on passe à la verif MAC

    if port.radius == 'COMMON' or port.radius == 'STRICT':
        # Authentification par mac
339
        interface = Interface.objects.filter(mac_address=mac_address).select_related('machine__user').select_related('ipv4').first()
340
        if not interface:
341
            room = port.room
342
            # On essaye de register la mac
343
            if not nas_type.autocapture_mac:
344 345 346
                return (sw_name, "", u'Machine inconnue', VLAN_NOK)
            elif not room:
                return (sw_name, "Inconnue", u'Chambre et machine inconnues', VLAN_NOK)
347
            else:
348 349
                if not room_user:
                    room_user = User.objects.filter(Q(club__room=port.room) | Q(adherent__room=port.room))
350
                if not room_user:
351
                    return (sw_name, room, u'Machine et propriétaire de la chambre inconnus', VLAN_NOK)
352
                elif room_user.count() > 1:
353
                    return (sw_name, room, u'Machine inconnue, il y a au moins 2 users dans la chambre/local -> ajout de mac automatique impossible', VLAN_NOK)
354
                elif not room_user.first().has_access():
355
                    return (sw_name, room, u'Machine inconnue et adhérent non cotisant', VLAN_NOK)
356
                else:
357
                    result, reason = room_user.first().autoregister_machine(mac_address, nas_type)
358
                    if result:
359
                        return (sw_name, room, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN)
360
                    else:
361
                        return (sw_name, room, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK)
362
        else:
363
            room = port.room
364
            if not interface.is_active:
365
                return (sw_name, room, u'Machine non active / adherent non cotisant', VLAN_NOK)
366 367
            elif not interface.ipv4:
                interface.assign_ipv4()
368
                return (sw_name, room, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN)
369
            else:
370
                return (sw_name, room, u'Machine OK' + extra_log, DECISION_VLAN)