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 19.6 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
options, created = OptionalTopologie.objects.get_or_create()
VLAN_NOK = options.vlan_decision_nok.vlan_id
VLAN_OK = options.vlan_decision_ok.vlan_id
66
VLAN_NON_MEMBER = options.vlan_non_member.vlan_id
67
RADIUS_POLICY = options.radius_general_policy
68

Gabriel Detraz's avatar
Gabriel Detraz committed
69 70 71 72
#: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))


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

87

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

96

Gabriel Detraz's avatar
Gabriel Detraz committed
97 98
def radius_event(fun):
    """Décorateur pour les fonctions d'interfaces avec radius.
99 100
    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
101 102 103 104 105 106 107 108 109 110
     * 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):
111 112
        """ The function transforming the tuples as dict """
        if isinstance(auth_data, dict):
Gabriel Detraz's avatar
Gabriel Detraz committed
113 114 115 116 117 118 119 120
            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:
121 122
            # 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
123 124 125 126 127 128 129
            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
@radius_event
def instantiate(*_):
    """Utile pour initialiser les connexions ldap une première fois (otherwise,
    do nothing)"""
    logger.info('Instantiation')
    if TEST_SERVER:
137
        logger.info(u'DBG_FREERADIUS is enabled')
Gabriel Detraz's avatar
Gabriel Detraz committed
138

139

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

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

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

188

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

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

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

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

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

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

253 254
    else:
        return radiusd.RLM_MODULE_OK
Gabriel Detraz's avatar
Gabriel Detraz committed
255

256

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

263

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

269

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

281

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


321 322 323 324
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.
325 326 327 328 329 330 331 332
    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
333
        - cotisation non à jour : VLAN_NON_MEMBER
334 335 336
        - sinon passe à common (ci-dessous)
    - mode common :
        - interface connue (macaddress):
337 338
            - utilisateur proprio non cotisant : VLAN_NON_MEMBER
            - utilisateur proprio banni : VLAN_NOK
339 340
            - user à jour : VLAN_OK
        - interface inconnue :
341
            - register mac désactivé : VLAN_NON_MEMBER
342
            - register mac activé :
343
                - dans la chambre associé au port, pas d'user ou non à
344
                  jour : VLAN_NON_MEMBER
345 346
                - user à jour, autocapture de la mac et VLAN_OK
    """
347
    # Get port from switch and port number
348
    extra_log = ""
349
    # Si le NAS est inconnu, on place sur le vlan defaut
350
    if not nas_machine:
351
        return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK)
352

353
    sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine)))
354

355 356 357 358 359 360
    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
361

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

368
    # On récupère le profil du port
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
369
    port_profile = port.get_port_profile
370

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

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

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

390 391
    # 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
392
    if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X':
393 394 395 396 397 398 399 400
        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
401
    if port_profile.radius_mode == 'STRICT':
402 403 404
        room = port.room
        if not room:
            return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK)
405

406 407 408
        room_user = User.objects.filter(
            Q(club__room=port.room) | Q(adherent__room=port.room)
        )
409
        if not room_user:
410
            return (sw_name, room, u'Chambre non cotisante', VLAN_NOK)
411
        for user in room_user:
412
            if user.is_ban() or user.state != User.STATE_ACTIVE:
413
                return (sw_name, room, u'Chambre resident desactive', VLAN_NOK)
414 415
            elif not (user.is_connected() or user.is_whitelisted()):
                return (sw_name, room, u'Utilisateur non cotisant', VLAN_NON_MEMBER)
416 417
        # else: user OK, on passe à la verif MAC

418
    # 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
419
    if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT':
420
        # Authentification par mac
421 422 423 424 425
        interface = (Interface.objects
                     .filter(mac_address=mac_address)
                     .select_related('machine__user')
                     .select_related('ipv4')
                     .first())
426
        if not interface:
427
            room = port.room
428 429
            # On essaye de register la mac, si l'autocapture a été activée
            # Sinon on rejette sur vlan_nok
430
            if not nas_type.autocapture_mac:
431
                return (sw_name, "", u'Machine inconnue', VLAN_NON_MEMBER)
432
            # On ne peut autocapturer que si on connait la chambre et donc l'user correspondant
433
            elif not room:
434 435 436
                return (sw_name,
                        "Inconnue",
                        u'Chambre et machine inconnues',
437
                        VLAN_NON_MEMBER)
438
            else:
439 440
                # Si la chambre est vide (local club, prises en libre services)
                # Impossible d'autocapturer
441
                if not room_user:
442 443 444
                    room_user = User.objects.filter(
                        Q(club__room=port.room) | Q(adherent__room=port.room)
                    )
445
                if not room_user:
446 447 448 449
                    return (sw_name,
                            room,
                            u'Machine et propriétaire de la chambre '
                            'inconnus',
450
                            VLAN_NON_MEMBER)
451 452
                # Si il y a plus d'un user dans la chambre, impossible de savoir à qui
                # Ajouter la machine
453
                elif room_user.count() > 1:
454 455 456 457 458
                    return (sw_name,
                            room,
                            u'Machine inconnue, il y a au moins 2 users '
                            'dans la chambre/local -> ajout de mac '
                            'automatique impossible',
459
                            VLAN_NON_MEMBER)
460
                # Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture
461
                elif not room_user.first().has_access():
462 463 464
                    return (sw_name,
                            room,
                            u'Machine inconnue et adhérent non cotisant',
465
                            VLAN_NON_MEMBER)
466
                # Sinon on capture et on laisse passer sur le bon vlan
467
                else:
468
                    interface, reason = (room_user
469 470 471 472 473
                                      .first()
                                      .autoregister_machine(
                                          mac_address,
                                          nas_type
                                      ))
474 475 476 477
                    if interface:
                        ## Si on choisi de placer les machines sur le vlan correspondant à leur type :
                        if RADIUS_POLICY == 'MACHINE':
                            DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
478 479 480 481
                        return (sw_name,
                                room,
                                u'Access Ok, Capture de la mac: ' + extra_log,
                                DECISION_VLAN)
482
                    else:
483 484 485
                        return (sw_name,
                                room,
                                u'Erreur dans le register mac %s' % (
486
                                    reason + str(mac_address)
487 488
                                ),
                                VLAN_NOK)
489 490 491
        # 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
492
        else:
493
            room = port.room
494
            if not interface.is_active:
495 496 497
                return (sw_name,
                        room,
                        u'Machine non active / adherent non cotisant',
498
                        VLAN_NON_MEMBER)
499 500 501 502
            ## Si on choisi de placer les machines sur le vlan correspondant à leur type :
            if RADIUS_POLICY == 'MACHINE':
                DECISION_VLAN = interface.type.ip_type.vlan.vlan_id
            if not interface.ipv4:
503
                interface.assign_ipv4()
504 505 506 507
                return (sw_name,
                        room,
                        u"Ok, Reassignation de l'ipv4" + extra_log,
                        DECISION_VLAN)
508
            else:
509 510 511 512
                return (sw_name,
                        room,
                        u'Machine OK' + extra_log,
                        DECISION_VLAN)