Ce serveur Gitlab sera éteint le 30 juin 2020, pensez à migrer vos projets vers les serveurs gitlab-research.centralesupelec.fr et gitlab-student.centralesupelec.fr !

auth.py 18.8 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
import os
import sys
40 41
import logging
import radiusd  # Module magique freeradius (radiusd.py is dummy)
42

43
from django.core.wsgi import get_wsgi_application
44
from django.db.models import Q
45

46 47 48 49 50 51 52 53 54 55 56
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.
application = get_wsgi_application()

57 58 59 60 61 62
from machines.models import Interface, IpList, Nas, Domain
from topologie.models import Port, Switch
from users.models import User
from preferences.models import OptionalTopologie


63 64 65 66
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
67 68 69 70
#: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))


71
# Logging
Gabriel Detraz's avatar
Gabriel Detraz committed
72 73 74 75 76 77 78 79 80 81 82 83 84
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)

85

Gabriel Detraz's avatar
Gabriel Detraz committed
86 87 88 89 90 91 92 93
# 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)

94

Gabriel Detraz's avatar
Gabriel Detraz committed
95 96
def radius_event(fun):
    """Décorateur pour les fonctions d'interfaces avec radius.
97 98
    Une telle fonction prend un uniquement argument, qui est une liste de
    tuples (clé, valeur) et renvoie un triplet dont les composantes sont :
Gabriel Detraz's avatar
Gabriel Detraz committed
99 100 101 102 103 104 105 106 107 108
     * 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):
109 110
        """ The function transforming the tuples as dict """
        if isinstance(auth_data, dict):
Gabriel Detraz's avatar
Gabriel Detraz committed
111 112 113 114 115 116 117 118
            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:
119 120
            # TODO s'assurer ici que les tuples renvoyés sont bien des
            # (str,str) : rlm_python ne digère PAS les unicodes
Gabriel Detraz's avatar
Gabriel Detraz committed
121 122 123 124 125 126 127
            return fun(data)
        except Exception as err:
            logger.error('Failed %r on data %r' % (err, auth_data))
            raise

    return new_f

128

Gabriel Detraz's avatar
Gabriel Detraz committed
129 130 131 132 133 134
@radius_event
def instantiate(*_):
    """Utile pour initialiser les connexions ldap une première fois (otherwise,
    do nothing)"""
    logger.info('Instantiation')
    if TEST_SERVER:
135
        logger.info(u'DBG_FREERADIUS is enabled')
Gabriel Detraz's avatar
Gabriel Detraz committed
136

137

Gabriel Detraz's avatar
Gabriel Detraz committed
138 139
@radius_event
def authorize(data):
140
    """On test si on connait le calling nas:
141 142
    - si le nas est inconnue, on suppose que c'est une requète 802.1X, on la
      traite
143
    - si le nas est connu, on applique 802.1X si le mode est activé
144 145
    - si le nas est connu et si il s'agit d'un nas auth par mac, on repond
      accept en authorize
146
    """
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 159 160 161 162
        result, log, password = check_user_machine_and_register(
            nas_type,
            user,
            mac
        )
163
        logger.info(log.encode('utf-8'))
164 165
        logger.info(user.encode('utf-8'))

166 167 168
        if not result:
            return radiusd.RLM_MODULE_REJECT
        else:
169 170 171 172 173 174
            return (
                radiusd.RLM_MODULE_UPDATED,
                (),
                (
                    (str("NT-Password"), str(password)),
                ),
175
            )
Gabriel Detraz's avatar
Gabriel Detraz committed
176

177
    else:
178 179 180 181 182 183
        return (
            radiusd.RLM_MODULE_UPDATED,
            (),
            (
                ("Auth-Type", "Accept"),
            ),
184
        )
Gabriel Detraz's avatar
Gabriel Detraz committed
185

186

Gabriel Detraz's avatar
Gabriel Detraz committed
187 188
@radius_event
def post_auth(data):
189 190 191
    """ Function called after the user is authenticated
    """

192
    nas = data.get('NAS-IP-Address', data.get('NAS-Identifier', None))
root's avatar
root committed
193
    nas_instance = find_nas_from_request(nas)
194
    # Toutes les reuquètes non proxifiées
195 196
    if not nas_instance:
        logger.info(u"Requète proxifiée, nas inconnu".encode('utf-8'))
197 198 199
        return radiusd.RLM_MODULE_OK
    nas_type = Nas.objects.filter(nas_type=nas_instance.type).first()
    if not nas_type:
200 201 202
        logger.info(
            u"Type de nas non enregistré dans la bdd!".encode('utf-8')
        )
203
        return radiusd.RLM_MODULE_OK
204

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

207 208
    # Switch et bornes héritent de machine et peuvent avoir plusieurs
    # interfaces filles
209
    nas_machine = nas_instance.machine
210
    # Si il s'agit d'un switch
211
    if hasattr(nas_machine, 'switch'):
212
        port = data.get('NAS-Port-Id', data.get('NAS-Port', None))
213 214
        # Pour les infrastructures possédant des switchs Juniper :
        # On vérifie si le switch fait partie d'un stack Juniper
215
        instance_stack = nas_machine.switch.stack
216 217 218
        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]
219 220 221 222 223
            nas_machine = (Switch.objects
                           .filter(stack=instance_stack)
                           .filter(stack_member_id=id_stack_member)
                           .prefetch_related(
                               'interface_set__domain__extension'
224
                           )
225 226 227
                           .first())
        # On récupère le numéro du port sur l'output de freeradius.
        # La ligne suivante fonctionne pour cisco, HP et Juniper
228
        port = port.split(".")[0].split('/')[-1][-2:]
229
        out = decide_vlan_and_register_switch(nas_machine, nas_type, port, mac)
230
        sw_name, room, reason, vlan_id = out
231

232
        log_message = '(fil) %s -> %s [%s%s]' % (
233
            sw_name + u":" + port + u"/" + str(room),
234 235 236 237
            mac,
            vlan_id,
            (reason and u': ' + reason).encode('utf-8')
        )
238 239 240
        logger.info(log_message)

        # Filaire
241 242
        return (
            radiusd.RLM_MODULE_UPDATED,
Gabriel Detraz's avatar
Gabriel Detraz committed
243 244 245
            (
                ("Tunnel-Type", "VLAN"),
                ("Tunnel-Medium-Type", "IEEE-802"),
246
                ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)),
Gabriel Detraz's avatar
Gabriel Detraz committed
247 248
            ),
            ()
249
        )
Gabriel Detraz's avatar
Gabriel Detraz committed
250

251 252
    else:
        return radiusd.RLM_MODULE_OK
Gabriel Detraz's avatar
Gabriel Detraz committed
253

254

255
# TODO : remove this function
Gabriel Detraz's avatar
Gabriel Detraz committed
256 257 258 259 260
@radius_event
def dummy_fun(_):
    """Do nothing, successfully. (C'est pour avoir un truc à mettre)"""
    return radiusd.RLM_MODULE_OK

261

Gabriel Detraz's avatar
Gabriel Detraz committed
262 263
def detach(_=None):
    """Appelé lors du déchargement du module (enfin, normalement)"""
264
    print("*** goodbye from auth.py ***")
Gabriel Detraz's avatar
Gabriel Detraz committed
265 266
    return radiusd.RLM_MODULE_OK

267

268
def find_nas_from_request(nas_id):
269
    """ Get the nas object from its ID """
270 271 272 273 274 275 276
    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
277
    return nas.first()
278

279

280
def check_user_machine_and_register(nas_type, username, mac_address):
281 282
    """Verifie le username et la mac renseignee. L'enregistre si elle est
    inconnue.
283 284 285 286 287
    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:
288 289 290
        return (False, u"User inconnu", '')
    if not user.has_access():
        return (False, u"Adhérent non cotisant", '')
291 292
    if interface:
        if interface.machine.user != user:
293 294 295 296
            return (False,
                    u"Machine enregistrée sur le compte d'un autre "
                    "user...",
                    '')
297 298
        elif not interface.is_active:
            return (False, u"Machine desactivée", '')
299 300 301
        elif not interface.ipv4:
            interface.assign_ipv4()
            return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm)
302
        else:
303
            return (True, u"Access ok", user.pwd_ntlm)
304
    elif nas_type:
305
        if nas_type.autocapture_mac:
306 307
            result, reason = user.autoregister_machine(mac_address, nas_type)
            if result:
308 309 310
                return (True,
                        u'Access Ok, Capture de la mac...',
                        user.pwd_ntlm)
311
            else:
312
                return (False, u'Erreur dans le register mac %s' % reason, '')
313 314
        else:
            return (False, u'Machine inconnue', '')
315
    else:
316
        return (False, u"Machine inconnue", '')
317 318


319 320 321 322
def decide_vlan_and_register_switch(nas_machine, nas_type, port_number,
                                    mac_address):
    """Fonction de placement vlan pour un switch en radius filaire auth par
    mac.
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    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é :
340 341
                - dans la chambre associé au port, pas d'user ou non à
                  jour : VLAN_NOK
342 343
                - user à jour, autocapture de la mac et VLAN_OK
    """
344
    # Get port from switch and port number
345
    extra_log = ""
346
    # Si le NAS est inconnu, on place sur le vlan defaut
347
    if not nas_machine:
348
        return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK)
349

350
    sw_name = str(nas_machine)
351

352 353 354 355 356 357
    port = (Port.objects
            .filter(
                switch=Switch.objects.filter(machine_ptr=nas_machine),
                port=port_number
            )
            .first())
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
358

359
    # Si le port est inconnu, on place sur le vlan defaut
360 361
    # Aucune information particulière ne permet de déterminer quelle
    # politique à appliquer sur ce port
362
    if not port:
363
        return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK)
364

365
    # On récupère le profil du port
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
366
    port_profile = port.get_port_profile
367

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
368
    # Si un vlan a été précisé dans la config du port,
369
    # on l'utilise pour VLAN_OK
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
370 371
    if port_profile.vlan_untagged:
        DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id)
372 373 374
        extra_log = u"Force sur vlan " + str(DECISION_VLAN)
    else:
        DECISION_VLAN = VLAN_OK
375

376
    # Si le port est désactivé, on rejette sur le vlan de déconnexion
377
    if not port.state:
378
        return (sw_name, port.room, u'Port desactivé', VLAN_NOK)
379

380
    # Si radius est désactivé, on laisse passer
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
381
    if port_profile.radius_type == 'NO':
382 383 384 385
        return (sw_name,
                "",
                u"Pas d'authentification sur ce port" + extra_log,
                DECISION_VLAN)
386

387 388
    # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment
    # Par conséquent, on laisse passer sur le bon vlan
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
389
    if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
390 391 392 393 394 395 396 397
        room = port.room or "Chambre/local inconnu"
        return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN)

    # Sinon, cela veut dire qu'on fait de l'auth radius par mac
    # Si le port est en mode strict, on vérifie que tous les users
    # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage)
    # Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis
    # dedans
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
398
    if port_profile.radius_mode == 'STRICT':
399 400 401
        room = port.room
        if not room:
            return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK)
402

403 404 405
        room_user = User.objects.filter(
            Q(club__room=port.room) | Q(adherent__room=port.room)
        )
406
        if not room_user:
407
            return (sw_name, room, u'Chambre non cotisante', VLAN_NOK)
408 409
        for user in room_user:
            if not user.has_access():
410
                return (sw_name, room, u'Chambre resident desactive', VLAN_NOK)
411 412
        # else: user OK, on passe à la verif MAC

413
    # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
414
    if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
415
        # Authentification par mac
416 417 418 419 420
        interface = (Interface.objects
                     .filter(mac_address=mac_address)
                     .select_related('machine__user')
                     .select_related('ipv4')
                     .first())
421
        if not interface:
422
            room = port.room
423 424
            # On essaye de register la mac, si l'autocapture a été activée
            # Sinon on rejette sur vlan_nok
425
            if not nas_type.autocapture_mac:
426
                return (sw_name, "", u'Machine inconnue', VLAN_NOK)
427
            # On ne peut autocapturer que si on connait la chambre et donc l'user correspondant
428
            elif not room:
429 430 431 432
                return (sw_name,
                        "Inconnue",
                        u'Chambre et machine inconnues',
                        VLAN_NOK)
433
            else:
434 435
                # Si la chambre est vide (local club, prises en libre services)
                # Impossible d'autocapturer
436
                if not room_user:
437 438 439
                    room_user = User.objects.filter(
                        Q(club__room=port.room) | Q(adherent__room=port.room)
                    )
440
                if not room_user:
441 442 443 444 445
                    return (sw_name,
                            room,
                            u'Machine et propriétaire de la chambre '
                            'inconnus',
                            VLAN_NOK)
446 447
                # Si il y a plus d'un user dans la chambre, impossible de savoir à qui
                # Ajouter la machine
448
                elif room_user.count() > 1:
449 450 451 452 453 454
                    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)
455
                # Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture
456
                elif not room_user.first().has_access():
457 458 459 460
                    return (sw_name,
                            room,
                            u'Machine inconnue et adhérent non cotisant',
                            VLAN_NOK)
461
                # Sinon on capture et on laisse passer sur le bon vlan
462
                else:
463 464 465 466 467 468
                    result, reason = (room_user
                                      .first()
                                      .autoregister_machine(
                                          mac_address,
                                          nas_type
                                      ))
469
                    if result:
470 471 472 473
                        return (sw_name,
                                room,
                                u'Access Ok, Capture de la mac: ' + extra_log,
                                DECISION_VLAN)
474
                    else:
475 476 477
                        return (sw_name,
                                room,
                                u'Erreur dans le register mac %s' % (
478
                                    reason + str(mac_address)
479 480
                                ),
                                VLAN_NOK)
481 482 483
        # L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject
        # Si elle n'a pas d'ipv4, on lui en met une
        # Enfin on laisse passer sur le vlan pertinent
484
        else:
485
            room = port.room
486
            if not interface.is_active:
487 488 489 490
                return (sw_name,
                        room,
                        u'Machine non active / adherent non cotisant',
                        VLAN_NOK)
491 492
            elif not interface.ipv4:
                interface.assign_ipv4()
493 494 495 496
                return (sw_name,
                        room,
                        u"Ok, Reassignation de l'ipv4" + extra_log,
                        DECISION_VLAN)
497
            else:
498 499 500 501
                return (sw_name,
                        room,
                        u'Machine OK' + extra_log,
                        DECISION_VLAN)