Projet

Général

Profil

authserver.py

version de /usr/share/sso/authserver.py (2.3) avec log du temps passé dans les fonctions de calcul - Bruno Boiget, 19/05/2016 16:12

Télécharger (62,6 ko)

 
1
#! /usr/bin/env python
2
# -*- coding: utf-8 -*-
3

    
4
###########################################################################
5
#
6
# Eole NG - 2007
7
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
8
# Licence CeCill  cf /root/LicenceEole.txt
9
# eole@ac-dijon.fr
10
#
11
# authserver.py
12
#
13
# serveur xmlrpc de gestion de session SSO Eole
14
#
15
###########################################################################
16
import os, shutil, time, traceback, urllib2, copy
17
import xmlrpclib
18
from glob import glob
19
# imports twisted
20
from twisted.internet import reactor, defer
21
# imports sso eole
22
import config
23
import timeit
24

    
25
# yes SessionManager is hard to disentangle, this is a hard standalone version
26
UNITTESTS_MODE = False
27
if UNITTESTS_MODE:
28
    _ = str
29
    def trace(func):
30
        return func
31
    from twisted.python import log
32
    default_idp_options = {}
33
    available_contexts = {}
34
    available_contexts['URN_PROTECTED_PASSWORD'] = 'urn_psswd'
35
    available_contexts['URN_TIME_SYNC_TOKEN'] = "urn_time_sync"
36
    #config.USE_SECURID = True
37
else:
38
    from page import trace, log
39
    #import de page.py pour pouvoir recharger les templates dans reload
40
    import page
41
    from saml_utils import get_metadata, date_from_string
42
    from saml_crypto import recreate_cert
43
    from saml_utils import available_contexts, default_idp_options
44

    
45
from eolesso.dataproxy import LDAPProxy
46
from eolesso.ticket import AppTicket
47
from eolesso.util import *
48
from eolesso.ticketcache import InvalidSession, TicketCache, SamlMsgCache
49
# import des fonctions de calcul de données utilisateur
50
import user_infos
51
# import des attributs de fédération externes
52
import external_attrs
53

    
54
if config.USE_SECURID or config.SECURID_PROVIDER:
55
    from securid_utils import SecuridUserStore
56

    
57
class SSOSessionManager(object):
58

    
59
    def __init__(self):
60
        self.address = config.AUTH_SERVER_ADDR
61
        self.init_caches()
62
        self._parent = None
63
        self.parent_url = None
64
        self._init_conf()
65

    
66
    def reload_conf(self):
67
        reload(user_infos)
68
        reload(external_attrs)
69
        reload(config)
70
        reload(page)
71
        self._init_conf()
72
        self.load_conf(reloading=True)
73
        return True
74

    
75
    def init_caches(self):
76
        # cache des "login tickets" (pour empêcher les navigateurs de 'rejouer' l'authentification avec un cache)
77
        self.login_sessions = TicketCache(180, self.address, 'LT')
78
        # cache des "granting tickets (ticket de session sso)"
79
        self.user_sessions = {}
80
        # cache des attributs utilisateur
81
        self.data_cache = {}
82
        # cache des attributs utilisateur (fonctions calculées non dynamiques)
83
        self.calc_cache = {}
84
        # cache des "application" et "proxy" tickets
85
        self.app_sessions = {}
86
        # dictionnaire d'association session utilisateur / tickets
87
        self.user_app_tickets = {}
88
        # cache pour la gestion des sessions de proxys (expirent en même temps que la session utilisateur)
89
        self.proxy_granting_sessions = {}
90
        # cache pour la gestion des sessions de fédération (fournisseur de service)
91
        self.saml_sessions = {}
92
        # caches de vérification des derniers messages envoyés/traités
93
        self.saml_sent_msg = SamlMsgCache(500)
94
        self.saml_rcved_msg = SamlMsgCache(500)
95
        # informations transitoires sur les sessions en cours de traitement
96
        self.relay_state_cache = SamlMsgCache(1000)
97
        # cache des checkers PAM (Securid)
98
        self.checkers = {}
99
        # cache pour la gestion du single logout (logouts en cours)
100
        # dans le cadre d'une requête de déconnexion globale: stockage de la session sso et du ticket de l'appelant
101
        # format : {'id_session': ( état_succes_saml,
102
        #                           [tickets_saml_deco_confirmée],
103
        #                           (ticket_appelant, id_req_logout, entite_saml_appelante),
104
        #                           {id_reponse_saml:ticket},
105
        #                           [services_cas_appelés])
106
        #          }
107
        self.pending_logout = {}
108
        # contextes d'authentification gérés (SAML 2)
109
        self.allowed_contexts = available_contexts
110

    
111
        if config.USE_SECURID or config.SECURID_PROVIDER:
112
            self.securid_user_store = SecuridUserStore()
113
        else:
114
            self.securid_user_store = None
115
            del(self.allowed_contexts['URN_TIME_SYNC_TOKEN'])
116
        if config.SECURID_PROVIDER:
117
            # cache des requêtes d'authentification OTP distantes en attente
118
            # Les demandes persistent 5 minutes car la procédure peut prendre
119
            # un peu de temps (saisie d'un 2ème code)
120
            self.securid_sessions = TicketCache(300, "", "_")
121

    
122
    def _init_conf(self):
123
        self.filters = {}
124
        self.apps = {'http':{'all':{}},
125
                     'https':{'all':{}},
126
                     'all':{'all':{}},
127
                     'sp_ident':{}}
128
        self.sp_meta = {}
129
        self.req_attributes = {}
130
        self.opt_attributes = {}
131
        self.attribute_sets = {}
132
        self.attribute_sets_ids = {}
133
        self.associations = {}
134
        static_data = {'rne': [config.RNE], 'nom_etab': [config.ETABLISSEMENT]}
135
        proxy = LDAPProxy(config.LDAP_SERVER, config.LDAP_PORT,
136
                          config.LDAP_BASE, config.LDAP_LABEL, config.LDAP_INFOS,
137
                          config.LDAP_READER, config.LDAP_READER_PASSFILE,
138
                          config.LDAP_LOGIN_OTP, static_data,
139
                          config.LDAP_MATCH_ATTRIBUTE)
140
        # lancement du serveur
141
        if config.PARENT_URL not in ('', None):
142
            self.parent_url = config.PARENT_URL
143
            self._parent = xmlrpclib.ServerProxy('%s/xmlrpc' % self.parent_url)
144
            log.msg("- %s : %s" % (_("Parent server defined"), self.parent_url))
145
        # proxy d'authentification
146
        self._data_proxy = proxy
147

    
148
    def load_conf(self, reloading=False):
149
        """Charge la configuration du serveur"""
150
        if not reloading:
151
            log.msg('* {0}'.format(_('loading server configuration')))
152
        # attributs calculés
153
        self.user_infos = user_infos
154
        log.msg("- %s : %s" % (_("Calculated Attributes defined"), ", ".join(user_infos.dict_infos.keys())))
155
        # attributs de fédération externes
156
        self.external_attrs = external_attrs.external_attrs
157
        log.msg("- %s : %s" % (_("External federation Attributes defined"), ", ".join(self.external_attrs.keys())))
158
        self.load_filters()
159
        # chargement des metadata SAML et jeux d'attributs
160
        self.load_attribute_sets()
161
        self.init_metadata()
162
        # informations sur les homonymes
163
        self.gen_homonymes_infos()
164
        # informations sur les établissements (etabs.js)
165
        update_etab_js(os.path.join(config.SSO_PATH,'interface','scripts','etabs.js'))
166
        if not reloading:
167
            # vérification de la validité des éventuels templates OTP
168
            if config.USE_SECURID or config.SECURID_PROVIDER:
169
                from securid_utils import get_otp_templates
170
                get_otp_templates(load = False)
171

    
172
    def check_ticket_issuer(self, ticket_id):
173
        """vérifie qu'un ticket a été généré par ce serveur
174
        """
175
        if ticket_id != '':
176
            ticket_address = "-".join(ticket_id.split('-')[1:-1])
177
            if ticket_address == self.address:
178
                return True
179
        return False
180

    
181
    def end_session(self, session_id):
182
        """termine la session d'un utilisateur
183
        """
184
        # supression des différents tickets associés
185
        if session_id in self.user_sessions:
186
            # recherche des tickets associées à la session
187
            for ticket in self.user_app_tickets[session_id]:
188
                # recherche des éventuels PGT associés à cet AppTicket
189
                if ticket.pgt != None:
190
                    del self.proxy_granting_sessions[ticket.pgt]
191
                # recherche des éventuels sessions distantes (saml : idp distant) associées
192
                if hasattr(ticket,'idp_session_index'):
193
                    if ticket.idp_session_index in self.saml_sessions:
194
                        del self.saml_sessions[ticket.idp_session_index]
195
                # self.user_app_tickets[session_id].remove(ticket)
196
                del self.app_sessions[ticket.ticket]
197
            if session_id in self.user_app_tickets:
198
                del self.user_app_tickets[session_id]
199
            if session_id in self.data_cache:
200
                del self.data_cache[session_id]
201
            if session_id in self.calc_cache:
202
                del self.calc_cache[session_id]
203
            if session_id in self.pending_logout:
204
                del self.pending_logout[session_id]
205
            del self.user_sessions[session_id]
206
            log.msg("%s -- %s" % (session_id, _("Terminating session")))
207

    
208
    def gen_saml_id(self, data={}, prefix='_'):
209
        msg_id = gen_random_id(prefix)
210
        while self.saml_sent_msg.get_msg(msg_id) != None:
211
            msg_id = gen_random_id(prefix)
212
        data['date_creation']=time.time()
213
        self.saml_sent_msg.add(msg_id, data)
214
        return msg_id
215

    
216
    def get_saml_msg(self, msg_id):
217
        return self.saml_sent_msg.get_msg(msg_id)
218

    
219
    def update_saml_msg(self, msg_id, data):
220
        assert msg_id in self.saml_sent_msg
221
        self.saml_sent_msg[msg_id].update(data)
222

    
223
    def replayed_saml_msg(self, msg_id):
224
        if msg_id in self.saml_rcved_msg:
225
            return True
226
        self.saml_rcved_msg.add(msg_id)
227
        return False
228

    
229
    def gen_relay_state(self, data, prefix='RS_'):
230
        msg_id = gen_random_id(prefix)
231
        while self.saml_sent_msg.get_msg(msg_id) != None:
232
            msg_id = gen_random_id(prefix)
233
        self.relay_state_cache.add(msg_id, data)
234
        return msg_id
235

    
236
    def get_relay_data(self, relay_state):
237
        return self.relay_state_cache.get_msg(relay_state)
238

    
239
    @trace
240
    def remove_old_ticket(self, session_id, saml_ident):
241
        """ deletes any existing session for a given partner entity
242
        """
243
        for ticket in self.user_app_tickets[session_id]:
244
            if getattr(ticket, 'saml_ident', '') == saml_ident:
245
                old_ticket = ticket.ticket
246
                # suppression du ticket des logout en cours si i len fait partie
247
                if session_id in self.pending_logout:
248
                    if old_ticket in self.pending_logout[session_id][1]:
249
                        self.pending_logout[session_id][1].remove(old_ticket)
250
                # suppression du ticket de la session utilisateur
251
                self.user_app_tickets[session_id].remove(ticket)
252
                # suppression des éventuelles indexs de session distants associés (authentification à travers un deuxième idp)
253
                if hasattr(ticket, 'idp_session_index'):
254
                    if ticket.idp_session_index in self.saml_sessions:
255
                        del(self.saml_sessions[ticket.idp_session_index])
256
                # suppression du ticket d'application
257
                del(self.app_sessions[old_ticket])
258
                del(ticket)
259
                break
260

    
261
    def load_filters(self):
262
        """chargement des filtres de données et applications
263
        """
264
        loaded_apps = []
265
        loaded_filters = []
266
        app_filters = set()
267
        # prise en compte des filtres globaux (attributs ajoutés à tous les filtres)
268
        glob_cfg = EoleParser()
269
        for conf_file in glob('%s/*.global' % config.FILTER_DIR):
270
            glob_cfg.read(conf_file)
271
        self.global_filter = dict((section, dict(glob_cfg.items(section))) for section in glob_cfg.sections())
272
        # filtres spécifiques à des applications et fournisseurs de services
273
        for conf_file in glob('%s/*.ini' % config.FILTER_DIR):
274
            conf_filename = os.path.splitext(os.path.basename(conf_file))[0]
275
            cfg = EoleParser()
276
            cfg.read(conf_file)
277
            if conf_filename.endswith('_apps') or conf_filename == 'applications':
278
                # liste des applications associées aux filtres
279
                new_apps, new_filters = self.load_applications(cfg)
280
                loaded_apps.extend(new_apps)
281
                app_filters.update(new_filters)
282
            else:
283
                # ajout d'un filtre de données
284
                app_filter = dict( (section, dict(cfg.items(section)))
285
                                   for section in cfg.sections() )
286
                self.filters[conf_filename] = app_filter
287
                loaded_filters.append(conf_filename)
288
        log.msg("- %s : %s" % (_("Filters loaded"), ", ".join(loaded_filters)))
289
        log.msg("- %s : %s" % (_("Applications defined"), ", ".join(loaded_apps)))
290
        # vérification de la présence de tous les filtres
291
        missing_filters = []
292
        for needed_filter in app_filters:
293
            if needed_filter not in self.filters:
294
                missing_filters.append(needed_filter)
295
        if missing_filters:
296
            log.msg('')
297
            log.msg('\t!! %s : %s' % (_('Following filters are needed but missing'), ', '.join(missing_filters)))
298
            log.msg('')
299

    
300
    def load_applications(self, cfg):
301
        """Chargement des applications associées aux filtres
302
        """
303
        loaded_apps = []
304
        filters = []
305
        for app in cfg.sections():
306
            try:
307
                app_infos = dict(cfg.items(app))
308
                # si un proxy particulier est déclaré, on vérifie qu'il est accessible
309
                use_proxy = app_infos.get('proxy', '')
310
                if ":" in use_proxy:
311
                    host, port = use_proxy.split(':')
312
                    if not check_url(host, port):
313
                        log.msg('\t! %s - %s : %s !' % (app, _('Warning, http proxy unreachable'), use_proxy))
314
                        use_proxy = ''
315
                # définition des filtres par entité de fournisseur de services (SAML)
316
                if 'sp_ident' in app_infos:
317
                    if 'sp_ident' in self.apps['sp_ident']:
318
                        log.msg(_("Warning, filter for service provider overwritten (already defined): %s") % str(app_infos['sp_ident']))
319
                    self.apps['sp_ident'][app_infos['sp_ident']] = (app_infos['filter'], use_proxy)
320
                    loaded_apps.append(app)
321
                else:
322
                    try:
323
                        port = app_infos['port'] = int(app_infos['port'])
324
                    except:
325
                        port = app_infos['port'] = 'all'
326
                    scheme = app_infos['scheme']
327
                    if scheme not in ['all', 'both']:
328
                        if scheme not in self.apps:
329
                            self.apps[scheme] = {'all':{}}
330
                        app_stores = [self.apps[scheme]]
331
                    elif scheme == 'both':
332
                        app_stores = [self.apps['http'], self.apps['https']]
333
                    else:
334
                        # pas de protocole spécifié, on stocke dans tous les protocoles
335
                        app_stores = self.apps.values()
336
                    # on trie les applications par port et chemin
337
                    for store in app_stores:
338
                        port_store = store.setdefault(port, {})
339
                        if app_infos['baseurl'] in port_store:
340
                            stored = False
341
                            for app_st in port_store[app_infos['baseurl']]:
342
                                if app_st[1] == app_infos['addr'] and app_st[2] == app_infos['typeaddr']:
343
                                    log.msg(_("Warning, application overwritten (already defined): %s") % (str(app_st)))
344
                                    app_st[3] = app_infos['filter']
345
                                    app_st[4] = use_proxy
346
                                    stored = True
347
                            if not stored:
348
                                port_store[app_infos['baseurl']].append([scheme, app_infos['addr'], app_infos['typeaddr'], app_infos['filter'], use_proxy])
349
                        else:
350
                            port_store[app_infos['baseurl']] = [[scheme,
351
                                                                 app_infos['addr'],
352
                                                                 app_infos['typeaddr'],
353
                                                                 app_infos['filter'],
354
                                                                 use_proxy]]
355
                        loaded_apps.append(app)
356
                        filters.append(app_infos['filter'])
357
            except Exception, e:
358
                log.msg("! %s %s : %s !" % (_("Error loading application"), app, e))
359
        return loaded_apps, filters
360

    
361
    def load_attribute_sets(self):
362
        """chargement des jeux d'attributs pour la fédération d'identité
363
        """
364
        self.check_eole_attr_sets()
365
        missing_indexes = []
366
        for conf_file in glob(os.path.join(config.ATTR_SET_DIR, '*.ini')):
367
            conf_filename = os.path.splitext(os.path.basename(conf_file))[0]
368
            if not conf_filename.startswith('associations'):
369
                cfg = EoleParser()
370
                cfg.read(conf_file)
371
                # ajout des jeux d'attributs (un seul par fichier)
372
                # on sépare les attributs en 2 parties:
373
                # user_attrs : attributs servant à créer le filtre de recherche de l'utilisateur
374
                # branch_attrs : attributs servant à déterminer un branche de recherche dans l'annuaire
375
                attr_set = {'user_attrs':{}, 'branch_attrs':{}, 'optional_attrs':{}}
376
                attr_set_index = None
377
                for section in cfg.sections():
378
                    # gestion de l'index du jeu d'attribut (si défini dans le fichier
379
                    if section == 'metadata':
380
                        try:
381
                            set_index = cfg.get(section, 'index')
382
                            assert int(set_index) >= 0
383
                            assert set_index not in self.attribute_sets_ids.values()
384
                            attr_set_index = set_index
385
                        except:
386
                            log.msg(_('invalid attribute set index for %s' ) % conf_filename)
387
                    elif section == 'branch_attrs':
388
                        attr_set[section] = dict(cfg.items(section))
389
                    elif section == 'optional':
390
                        for dist_attr, local_attr in cfg.items(section):
391
                            attr_set['optional_attrs'][dist_attr] = local_attr
392
                    else:
393
                        # tous les autres attributs sont considérés comme requis
394
                        for dist_attr, local_attr in cfg.items(section):
395
                            attr_set['user_attrs'][dist_attr] = local_attr
396
                # gestion de l'index du jeu d'attribut
397
                if attr_set_index is None:
398
                    self.attribute_sets_ids[conf_filename] = None
399
                    missing_indexes.append(conf_filename)
400
                else:
401
                    self.attribute_sets_ids[conf_filename] = attr_set_index
402
                self.attribute_sets[conf_filename] = attr_set
403
        # options par défaut
404
        self.associations['default'] = {}
405
        self.associations['default'].update(default_idp_options)
406
        for assoc_file in glob('%s/associations*.ini' % config.ATTR_SET_DIR):
407
            # chargement des associations <entité partenaire><set d'attributs>
408
            cfg = EoleParser()
409
            cfg.read(assoc_file)
410
            for entity_id in cfg.sections():
411
                assoc = self.associations.get(entity_id, {})
412
                # XXX FIXME : permettre d'associer un filtre ici au lieu des filtres avec saml_ident ?
413
                opts_path = {'attribute_set':config.ATTR_SET_DIR, 'filter':config.FILTER_DIR}
414
                for option, value in cfg.items(entity_id):
415
                    if option in ('attribute_set', 'filter'):
416
                        data_file = os.path.join(opts_path[option], '%s.ini' % value)
417
                        if not os.path.isfile(data_file):
418
                            log.msg('%s : %s %s' % (assoc_file, data_file, _('not found')))
419
                            continue
420
                    assoc[option] = value
421
                self.associations[entity_id] = assoc
422
                # Vérification sur les attributs non compatibles
423
                if is_true(assoc.get('passive', '')) and is_true(assoc.get('force_auth', '')):
424
                    log.msg('\t! %s : %s !' % (entity_id, _('Warning, force_auth and passive are mutually exclusive options, force_auth set to false')))
425
                    assoc['force_auth'] = 'false'
426
        for opt, val in self.associations['default'].items():
427
            if val != default_idp_options.get(opt, None):
428
                if opt == 'attribute_set':
429
                    log.msg(_('default attribute set redefined : %s') % val)
430
                else:
431
                    log.msg(_('default value for option %s defined to : %s') % (opt, val))
432
        # calcul et stockage des indexs manquants
433
        if missing_indexes:
434
            for set_id in range(len(self.attribute_sets)):
435
                set_id = str(set_id)
436
                if set_id not in self.attribute_sets_ids.values():
437
                    attr_set = missing_indexes.pop(0)
438
                    # attribution de l'index au premier set n'en ayant pas
439
                    self.store_set_index(set_id, attr_set)
440
                    self.attribute_sets_ids[attr_set] = set_id
441
                    if len(missing_indexes) == 0:
442
                        # tous les sets ont un index
443
                        break
444

    
445
    def store_set_index(self, index, setname):
446
        """enregistre l'index du jeu d'attribut dans le fichier de définition"""
447
        conf_file = os.path.join(config.ATTR_SET_DIR, "%s.ini" % setname)
448
        cfg = EoleParser()
449
        cfg.read(conf_file)
450
        if not cfg.has_section('metadata'):
451
            cfg.add_section('metadata')
452
        cfg.set('metadata', 'index', index)
453
        try:
454
            f_conf = open(conf_file, 'w')
455
            cfg.write(f_conf)
456
            f_conf.close()
457
            log.msg(_('index %s assigned to attribute set %s') % (index, setname))
458
        except IOError:
459
            log.msg(_('error updating configuration file %s') % conf_file)
460
            return False
461
        return True
462

    
463
    def check_eole_attr_sets(self):
464
        """vérifie la cohérence des jeux d'attributs livrés par EoleSSO"""
465
        log.msg('- {0}'.format(_('updating default attribute_sets')))
466
        for eole_file in glob(os.path.join(config.ATTR_SET_DIR, "eole", "*.ini")):
467
            dest_file = os.path.join(config.ATTR_SET_DIR, os.path.basename(eole_file))
468
            if not os.path.isfile(dest_file):
469
                # fichier non présent, on reprend le fichier fourni
470
                shutil.copy(eole_file, dest_file)
471
            else:
472
                # fichier déjà en place, on met à jour le contenu sans modifier l'index du jeu d'attribut
473
                eole_set = EoleParser()
474
                eole_set.read(eole_file)
475
                dest_set = EoleParser()
476
                dest_set.read(dest_file)
477
                # on reprend l'index dans le fichier actuel si présent
478
                if dest_set.has_option('metadata', 'index'):
479
                    eole_set.add_section('metadata')
480
                    # on reprend l'indice du fichier de destination si il existe déjà
481
                    current_index = dest_set.get('metadata', 'index')
482
                    eole_set.set('metadata', 'index', current_index)
483
                # on sauvegarde  le contenu du fichier eole sur le fichier de destination
484
                f_dest = open(dest_file, 'w')
485
                eole_set.write(f_dest)
486
                f_dest.close()
487

    
488

    
489
    def init_metadata(self):
490
        associations_updated = False
491
        # réinitialisation des certificats provenant des metadata
492
        cert_dir = os.path.join(config.METADATA_DIR,'certs')
493
        if os.path.isdir(cert_dir):
494
            shutil.rmtree(cert_dir)
495
        os.makedirs(cert_dir)
496
        for conf_file in glob('%s/*.xml' % config.METADATA_DIR):
497
            saml_ident = os.path.splitext(os.path.basename(conf_file))[0]
498
            self.load_metadata(saml_ident)
499

    
500
    @trace
501
    def load_metadata(self, saml_ident):
502
        try:
503
            sp_meta = get_metadata(saml_ident)
504
            if 'entityID' in sp_meta:
505
                saml_ident = sp_meta['entityID']
506
            if type(saml_ident) == unicode:
507
                saml_ident = saml_ident.encode(config.encoding)
508
            f_cert = os.path.join(config.METADATA_DIR,'certs','%s.crt' % saml_ident.replace(os.sep,'_'))
509
            if not os.path.isfile(f_cert):
510
                # stockage des certificats sous forme de fichiers pem
511
                cert_data = None
512
                if 'SignCert' in sp_meta:
513
                    cert_data = sp_meta['SignCert']
514
                elif 'SignCert' in sp_meta.get('SPSSODescriptor',{}):
515
                    cert_data = sp_meta['SPSSODescriptor']['SignCert']
516
                elif 'SignCert' in sp_meta.get('IDPSSODescriptor',{}):
517
                    cert_data = sp_meta['IDPSSODescriptor']['SignCert']
518
                if cert_data:
519
                    cert_buffer = recreate_cert(cert_data)
520
                    # stockage du certificat de signature
521
                    f = open(f_cert, 'w')
522
                    f.write(cert_buffer)
523
                    f.close()
524
        except Exception, e:
525
            log.msg("- %s (%s)" % (_("Error fetching SAML metadata for %s : ") % saml_ident, str(e)))
526
            return {}
527
        if saml_ident not in self.sp_meta:
528
            attr_set = self.associations.get(saml_ident, {}).get('attribute_set', '')
529
            if attr_set not in ('', 'default'):
530
                attr_msg = ' (%s : %s)' % (_('attribute set'), attr_set)
531
            else:
532
                attr_msg = ''
533
            log.msg("- %s : %s%s" % (_("Partner entity initialized"), saml_ident, attr_msg))
534
        # mise à jour des métadonnées
535
        self.sp_meta[saml_ident] = sp_meta
536
        return sp_meta
537

    
538
    @trace
539
    def get_metadata(self, saml_ident):
540
        if saml_ident in self.sp_meta:
541
            return self.sp_meta[saml_ident]
542
        else:
543
            try:
544
                sp_meta = self.load_metadata(saml_ident)
545
            except:
546
                sp_meta = {}
547
        return sp_meta
548

    
549
    @trace
550
    def get_attribute_set(self, idp_ident, search_branch=None):
551
        attr_set = {'user_attrs':{}, 'branch_attrs':{}, 'optional_attrs':{}}
552
        if idp_ident == config.IDP_IDENTITY:
553
            # cas particulier : utilisation pour retrouver un utilisateur local
554
            # (si recherche suite à une méthode d'authentification non LDAP)
555
            if search_branch:
556
                # on récupère si possible l'attribut défini comme clé de recherche
557
                try:
558
                    host, base_ldap = search_branch.split(":",1)
559
                    default_userattr = self._data_proxy.ldap_servers[host][3]
560
                except:
561
                    default_userattr = 'uid'
562
            attr_set['user_attrs'][default_userattr] = default_userattr
563
            attr_set['branch_attrs'].update(config.SEARCH_BASE_ATTRS)
564
        else:
565
            try:
566
                # recherche un jeu d'attributs associés à ce fournisseur de service (ou jeu d'attribut par défaut)
567
                attr_set_name = self.associations.get(idp_ident, {}).get('attribute_set', self.associations['default']['attribute_set'])
568
                # mise à jour du jeu d'attribut temporaire (il est susceptible d'être modifié en cas d'attribut externe)
569
                for attr_section in self.attribute_sets[attr_set_name]:
570
                    attr_set[attr_section].update(self.attribute_sets[attr_set_name][attr_section])
571
            except:
572
                log.msg(_('Unable to determine attribute set for %s') % idp_ident)
573
        return attr_set
574

    
575
    def gen_homonymes_infos(self):
576
        """Génère un fichier javascript contenant des
577
        informations à afficher dans le cas ou des homonymes sont détectés
578
        """
579
        h_dir = config.HOMONYMES_DIR
580
        h_script = os.path.join(config.SSO_PATH, 'interface', 'scripts', 'homonymes.js')
581
        script = """var msgs=new Array();\nvar host_infos=new Array();\n"""
582
        infos_used = {}
583
        host_infos = []
584
        index_msg = 1
585
        for host, infos in self._data_proxy.ldap_infos.items():
586
            if infos and os.path.isfile(os.path.join(h_dir, infos)):
587
                if infos not in infos_used:
588
                    msg = open(os.path.join(h_dir, infos)).read().strip().replace('"', "'")
589
                    infos_used[infos] = index_msg
590
                    script += """msgs['msg%s']="%s";\n""" % (str(index_msg), msg)
591
                    index_msg += 1
592
                host_infos.append("""host_infos['%s']="msg%s";""" % (host, infos_used[infos]))
593
        if host_infos:
594
            script += "\n".join(host_infos);
595
        f_script = open(h_script, 'w')
596
        f_script.write(script)
597
        f_script.close()
598

    
599
    @trace
600
    def get_default_logout_url(self, sso_session):
601
        default_url = None
602
        force_redirect = False
603
        if sso_session in self.user_sessions:
604
            idp_ident = self.user_sessions[sso_session][3]
605
            if idp_ident and (idp_ident != config.IDP_IDENTITY):
606
                default_url = self.associations.get(idp_ident, {}).get('default_logout_url', None)
607
                if default_url:
608
                    # url configurée, on regarde si son utilisation doit être forcée
609
                    # (autres url passées en paramètres ignorées)
610
                    force_redirect = is_true(self.associations.get(idp_ident, {}).get('force_logout_url', 'false'))
611
        return default_url, force_redirect
612

    
613
    @trace
614
    def get_federation_options(self, idp_ident):
615
        """renvoie les options configurées pour l'accord de fédération avec un fournisseur d'identité
616

617
        Les options gérées sont:
618

619
        - attribute_set : jeu d'attribut permettant de retrouver l'utilisateur
620
        - sign_request : force la signature des requêtes vers cet idp (false par défaut sauf si indiqué dans les métadata du FI)
621
        - passive : indique une requête de type passive (si l'utilisateur n'est pas authentifié, réponse négative)
622
        - force_auth : force une réauthentification de l'utilisateur même si il était préalablement connecté
623
        - allow_idp : interdit la prise en compte de toute les assertions provenant d'un FI
624
        - allow_idp_initiated : interdit la prise en compte des assertions envoyées spontanément par un FI
625
        - default_service : adresse d'un service utilisé par défaut après une authentification réussie
626
        - req_context : contexte d'authentification requis pour valider l'authentification provenant d'une assertion
627
        - comparison : opérateur de comparaison pour le contexte ci dessus (minimum, maximum, better, exact)
628
        """
629
        # prise en compte des options par défaut
630
        options = self.associations.get('default', {})
631
        # options définies pour cette entité
632
        options.update(self.associations.get(idp_ident, {}))
633
        return options
634

    
635
    @trace
636
    def check_federation_allowed(self, idp_ident, idp_initiated=False):
637
        """vérifie qu'un fournisseur d'identité est autorisé à fournir des assertions
638
        idp_initiated : vérifie si les réponses non sollicitées sont autorisées (pas de requête préalable)
639
        """
640
        default_allow_idp = self.associations['default'].get('allow_idp', 'true')
641
        default_allow_idp_initiated = self.associations['default'].get('allow_idp_initiated', 'true')
642
        if idp_ident in self.associations:
643
            assoc_data = self.associations.get(idp_ident, {})
644
            if idp_initiated:
645
                # assertions spontanées autorisées par défaut si rien n'est spécifié
646
                if not is_true(assoc_data.get('allow_idp_initiated', default_allow_idp_initiated)):
647
                    return False
648
            # assertions autorisées par défaut si pas de mention contraire
649
            return is_true(assoc_data.get('allow_idp', default_allow_idp))
650
        # pas d'informations données, utilise les autorisations par défaut
651
        if is_true(default_allow_idp):
652
            if not idp_initiated or is_true(default_allow_idp_initiated):
653
                return True
654
        return False
655

    
656
    @trace
657
    def get_attribute_service_index(self, idp_ident):
658
        set_name = self.associations.get(idp_ident, {}).get('attribute_set', 'default')
659
        return self.attribute_sets_ids.get(set_name, None)
660

    
661
    @trace
662
    def init_user_session(self, user_data, username, auth_instant , search_attrs=None, auth_class=available_contexts['URN_PROTECTED_PASSWORD'], idp_ident=None):
663
        sessionID = gen_ticket_id('TGC', self.address)
664
        if sessionID != "":
665
            # XXX FIXME : utilisation de la clé de fédération comme nom d'utilisateur CAS ?
666
            if search_attrs is not None:
667
                self.user_sessions[sessionID] = [search_attrs, auth_instant, auth_class, idp_ident]
668
            else:
669
                self.user_sessions[sessionID] = [username, auth_instant, auth_class, idp_ident]
670
            # stockage des données utilisateur
671
            for ignored_attr in config.IGNORED_ATTRS:
672
                if ignored_attr in user_data:
673
                    del(user_data[ignored_attr])
674
            self.data_cache[sessionID] = user_data
675
            self.calc_cache[sessionID] = {}
676
            # initialisation de la liste des tickets associés
677
            self.user_app_tickets[sessionID] = []
678
            reactor.callLater(config.SESSION_TIMER, self.end_session, sessionID)
679
        return sessionID
680

    
681
    @trace
682
    def authenticate_federated_user(self, attrs, idp_ident, auth_instant, auth_class, current_session=None, search_branch=None):
683
        # recherche du jeu d'attributs à utiliser
684
        user_data = {}
685
        optional_attrs = {}
686
        # création d'une copie du jeu d'attribut passé aux fonctions
687
        # de calcul d'attributs externes pour mise à jour
688
        attr_set = self.get_attribute_set(idp_ident, search_branch)
689
        user_attrs = {}
690
        user_attrs.update(attrs)
691
        idp_attrs = {}
692
        # si des attributs 'externes' sont présents,
693
        # on les traite avant de chercher les occurences LDAP
694
        for idp_attr, value in attrs.items():
695
            if idp_attr in self.external_attrs.keys():
696
                ext_attrs = self.external_attrs[idp_attr].get_local_attrs(copy.copy(value), attr_set)
697
                # on remplace l'attribut par ceux retournés
698
                idp_attrs.update(ext_attrs)
699
            else:
700
                idp_attrs[idp_attr] = value
701
        # recherche de l'utilisateur en fonction du jeu d'attributs
702
        if search_branch is None:
703
            search_branch = self._data_proxy.get_search_branch(idp_attrs, attr_set)
704
        if search_branch is None:
705
            log.msg(_("Federation - Unable to determine search branch with following attributes : "), idp_attrs)
706
        else:
707
            try:
708
                # si branche non retrouvée, on considère que la fédération a échoué
709
                search_attrs = {}
710
                search_base = None
711
                # on parcourt les attributs reçus
712
                for idp_attr, value in idp_attrs.items():
713
                    if idp_attr in attr_set['user_attrs']:
714
                        # correspond à la clé de fédération
715
                        if len(value) > 0:
716
                            search_attrs[attr_set['user_attrs'][idp_attr]] = value[0]
717
                    if idp_attr in attr_set['optional_attrs']:
718
                        # attributs supplémentaires, on les ajoutera
719
                        # au données utilisateur (écrasés si présents localement ?)
720
                        optional_attrs[attr_set['optional_attrs'][idp_attr]] = value
721
                # tous les attributs doivent correspondre à ceux demandés dans le set d'attributs
722
                if search_attrs and (len(search_attrs) == len(attr_set['user_attrs'])):
723
                    log.msg(_('Federation: searching user matching attributes %s (search branch: %s)') % (str(search_attrs), search_branch))
724
                    defer_fed_data = self._data_proxy.get_user_data(search_attrs, search_branch)
725
                    return defer_fed_data.addCallbacks(self.callb_federation, self.errb_auth,
726
                           callbackArgs=[search_attrs, optional_attrs, idp_ident, auth_instant, auth_class, current_session])
727
            except:
728
                traceback.print_exc()
729
                user_data = {}
730
        return defer.succeed(('', user_data))
731

    
732
    def set_urllib_proxy(self, ticket=None):
733
        """Défini un proxy http pour les appels via urllib2
734
        ticket: ticket applicatif ou rien pour supprimer les proxys
735
        """
736
        urllib_proxy = None
737
        if ticket:
738
            proxy_app = getattr(ticket, 'use_proxy', '')
739
            try:
740
                if proxy_app == 'default':
741
                    assert config.PROXY_SERVER and config.PROXY_PORT
742
                    urllib_proxy = {'https':"http://%s:%s" % (config.PROXY_SERVER, config.PROXY_PORT)}
743
                elif ":" in proxy_app:
744
                    # proxy particulier défini sous la forme "serveur:port"
745
                    urllib_proxy = {'https':"http://%s:%s" % tuple(proxy_app.split(':'))}
746
            except:
747
                log.msg(_('Could not determine http proxy for ticket %s (service: %s)') % (ticket.ticket, ticket.service_url))
748
        # mise en place du proxy au niveau de la librairie
749
        opener = urllib2.build_opener(urllib2.ProxyHandler(urllib_proxy))
750
        urllib2.install_opener(opener)
751

    
752
    def get_user_log_data(self, user_data, additional_attrs=[]):
753
        log_attrs = []
754
        display_attrs = ['uid', 'cn']
755
        display_attrs.extend(additional_attrs)
756
        for log_attr in display_attrs:
757
            if log_attr in user_data:
758
                log_attrs.append("%s: %s" % (log_attr, user_data[log_attr][0]))
759
        return ", ".join(log_attrs)
760

    
761
    @trace
762
    def callb_federation(self, result, search_attrs, optional_attrs, idp_ident, auth_instant, auth_class, current_session):
763
        bind_success, user_data = result
764
        sessionID = ''
765
        if current_session:
766
            if self.user_sessions[current_session][0] == search_attrs:
767
                # session déjà existante pour cet utilisateur
768
                return current_session, user_data
769
        first_fed_key = search_attrs.keys()[0]
770
        if first_fed_key in user_data:
771
            # utilisateur local trouvé: création de la session
772
            username = user_data[first_fed_key][0]
773
            # si des attributs optionels sont présents, on  les intègre dans les attributs utilisateur
774
            # les attributs locaux écrasent les attributs reçus si ils sont présents
775
            if optional_attrs:
776
                optional_attrs.update(user_data)
777
                user_data = optional_attrs
778
            # description de l'utilisateur trouvé dans les logs
779
            sessionID = self.init_user_session(user_data, username, auth_instant, search_attrs, auth_class, idp_ident)
780
            log.msg(_('Federation: user found (%s). Session ID : %s') % (self.get_user_log_data(user_data, [first_fed_key]), sessionID))
781
        else:
782
            log.msg(_('Federation: user from %s not found (%s)') % (idp_ident, str(search_attrs)))
783
        return sessionID, user_data
784

    
785
    def authenticate(self, username, password, search_branch='default'):
786
        user_data = {}
787
        sessionID = ''
788
        if type(username) == unicode: username = username.encode(config.encoding)
789
        if type(password) == unicode: password = password.encode(config.encoding)
790

    
791
        # on demande en premier au serveur père qu'il vérifie les informations
792
        if self._parent is not None:
793
            try:
794
                sessionID, user_data = self._parent.authenticate(username, password, search_branch)
795
            except Exception, e:
796
                log.msg(_('Error calling function %s on parent server : %s') % ('authenticate', str(e)))
797
                pass
798
        if sessionID != '':
799
            log.msg(_("Session delivered by parent serveur for %s. Session ID : %s") % (username, sessionID))
800
            self.data_cache[sessionID] = user_data
801
            # les attributs calculés ne sont pas renvoyés par le parent
802
            self.calc_cache[sessionID] = {}
803
            # session du parent, on crée une session locale 'déportée'
804
            # on ne gère pas d'expiration sur cette session, la validité sera vérifiée sur le parent
805
            self.user_sessions[sessionID] = [None,None,None,None]
806
            self.user_app_tickets[sessionID] = []
807
        elif password != '':
808
            # non authentifié par un parent, on essaye de le faire localement
809
            defer_auth = self._data_proxy.authenticate(username, password, search_branch)
810
            return defer_auth.addCallbacks(self.callb_auth, self.errb_auth, callbackArgs=[username])
811
        return defer.succeed((sessionID, user_data))
812

    
813
    @trace
814
    def errb_auth(self, failure):
815
        log.msg(_("! Error accessing Authentication service (%s) !") % self._data_proxy.service_name)
816
        log.msg(failure.getTraceback())
817
        return '', {}
818

    
819
    @trace
820
    def callb_auth(self, res_auth, username):
821
        """traitement du retour de la méthode d'authentification de dataproxy
822
        """
823
        # on stocke le timestamp de la date d'authentification
824
        auth_instant = time.time()
825
        # par défaut, on défini la classe d'authentification comme
826
        # saisie de mot de passe sur un support sécurisé (https)
827
        # cet attribut sera modifié par le serveur en cas de validation d'un
828
        # type d'authentification plus élevé (i.e clé OTP).
829
        auth_class = available_contexts['URN_PROTECTED_PASSWORD']
830
        authenticated, user_data = res_auth
831
        if authenticated:
832
            sessionID = self.init_user_session(user_data, username, auth_instant, auth_class=auth_class)
833
            # description de l'utilisateur trouvé dans les logs
834
            log.msg(_('user authentication verified (%s). Session ID : %s (%i sessions)') % \
835
                    (self.get_user_log_data(user_data),sessionID,len(self.user_sessions)))
836
            return sessionID, user_data
837
        log.msg(_("! Authentication failure : %s !") % username)
838
        return '', {}
839

    
840
    def get_login_ticket(self, data=''):
841
        return self.login_sessions.add_session(data)
842

    
843
    @trace
844
    def validate_session(self, session_id):
845
        """vérifie un session_id existant"""
846
        if self.check_ticket_issuer(session_id) and session_id in self.user_sessions:
847
            return True
848
        if self._parent is not None:
849
            try:
850
                valid_session = self._parent.validate_session(session_id)
851
            except Exception, e:
852
                log.msg(_('Error calling function %s on parent server : %s') % ('validate_session', str(e)))
853
                # XXX FIXME : différencier socket.error ?
854
                valid_session = False
855
            if valid_session:
856
                log.msg("%s : %s" % (session_id, _("User session validated on parent server")))
857
                # si la session n'est pas présente localement, mais validée par le parent, on l'ajoute (ex : redémarrage du serveur local)
858
                if not session_id in self.user_sessions:
859
                    self.user_sessions[session_id] = [None,None,None,None]
860
                    self.user_app_tickets[session_id] = []
861
            return valid_session
862
        log.msg("! %s : %s !" % (_("Invalid session"), session_id))
863
        return False
864

    
865
    @trace
866
    def verify_session_id(self, session_id):
867
        """vérifie un session_id"""
868
        if self.check_ticket_issuer(session_id) and session_id in self.user_sessions:
869
            return True, session_id
870
        if self._parent is not None:
871
            try:
872
                return self._parent.verify_session_id(session_id)
873
            except Exception, e:
874
                log.msg(_('Error calling function %s on parent server : %s') % ('verify_session', str(e)))
875
                return False, session_id
876
        log.msg("! %s : %s !" % (_("Unknown user session"), session_id))
877
        return False, session_id
878

    
879
    @trace
880
    def get_user_info(self, session_id, details=False):
881
        """méthode de convenance pour récupérer l'utilisateur"""
882
        if session_id in self.user_sessions:
883
            if self.check_ticket_issuer(session_id):
884
                if details:
885
                    return self._get_user_details(session_id, "", {})
886
                else:
887
                    return self.user_sessions[session_id][0]
888
            else:
889
                try:
890
                    # session du serveur parent
891
                    return self._parent.get_user_info(session_id, details)
892
                except:
893
                    pass
894
        raise InvalidSession(session_id)
895

    
896
    @trace
897
    def get_auth_instant(self, app_ticket):
898
        """renvoie la date à laquelle l'authentification a eu lieu
899
        """
900
        session_id = app_ticket
901
        ticket = app_ticket
902
        if app_ticket in self.app_sessions and self.app_sessions[app_ticket] is not None:
903
            ticket = self.app_sessions[app_ticket]
904
            session_id = ticket.session_id
905
            if session_id in self.user_sessions:
906
                return self.user_sessions[session_id][1]
907
        else:
908
            # ticket inconnu, essai de vérification sur un serveur parent
909
            if self._parent is not None:
910
                try:
911
                    return self._parent.get_auth_instant(app_ticket)
912
                except Exception, e:
913
                    log.msg(_('Error calling function %s on parent server : %s') % ('verify_app_ticket', str(e)))
914
                    pass
915
        raise InvalidSession(session_id)
916

    
917
    @trace
918
    def get_auth_class(self, app_ticket):
919
        """retourne la classe d'authentification d'une session"""
920
        session_id = app_ticket
921
        ticket = app_ticket
922
        if app_ticket in self.app_sessions and self.app_sessions[app_ticket] is not None:
923
            ticket = self.app_sessions[app_ticket]
924
            session_id = ticket.session_id
925
            if session_id in self.user_sessions:
926
                return self.user_sessions[session_id][2]
927
        if self._parent is not None:
928
            try:
929
                return self._parent.get_auth_class(app_ticket)
930
            except Exception, e:
931
                log.msg(_('Error calling function %s on parent server : %s') % ('get_auth_class', str(e)))
932
                return None
933
        log.msg("! %s : %s !" % (_("Unknown user session"), session_id))
934
        return None
935

    
936
    @trace
937
    def set_auth_class(self, session_id, auth_class):
938
        """modifie la classe d'authentification pour une session donnée"""
939
        if session_id in self.user_sessions:
940
            self.user_sessions[session_id][2] = auth_class
941
            log.msg(_('updating authentication context for session %s : %s') % (session_id, auth_class))
942
            return True
943
        return False
944

    
945
    def logout(self, session_id):
946
        """vérifie un session_id"""
947
        if session_id in self.user_sessions:
948
            self.end_session(session_id)
949
            if not self.check_ticket_issuer(session_id):
950
                # session délivrée par le parent ?
951
                try:
952
                    return self._parent.logout(session_id)
953
                except Exception, e:
954
                    log.msg(_('Error calling function %s on parent server : %s') % ('logout', str(e)))
955
                    pass
956
            else:
957
                return 'ok'
958
        return ''
959

    
960
    @trace
961
    def get_app_ticket(self, session_id, appurl, ticket_prefix='ST', from_credentials=False, idp_ident=None):
962
        # si cette session utilisateur est valide, on crée un ticket pour l'application
963
        appSessionID = ''
964
        if self.validate_session(session_id):
965
            # génération d'un nouveau ticket d'application associé à la session
966
            appSessionID = gen_ticket_id(ticket_prefix, self.address)
967
            ticket = self.init_app_session(appSessionID, session_id, appurl, from_credentials)
968
            if ticket.filter != '':
969
                filter_msg = ' (%s : %s)' % (_("attribute filter"), ticket.filter)
970
            else:
971
                filter_msg = ''
972
            if idp_ident:
973
                # ticket d'information sur une fédération établie
974
                log.msg("%s -- %s" % (session_id, _("federation information stored (identity provider: %s)") % idp_ident))
975
            else:
976
                log.msg("%s -- %s %s%s" % (session_id, _("Session authorized for service"), ticket.service_url, filter_msg))
977
            return appSessionID
978
        else:
979
            log.msg("%s : %s" % (_("Unknown user session"), session_id))
980
        log.msg("! %s -- %s %s !" % (session_id, _("Failed to create session for service"), appurl))
981
        return ''
982

    
983
    @trace
984
    def init_app_session(self, appSessionID, session_id, appurl, from_credentials):
985
        """génère un ticket d'application et initialise son timeout
986
        """
987
        ticket = AppTicket(appSessionID, session_id, appurl, self.address, from_credentials=from_credentials)
988
        # détection d'un éventuel filtre sur les données utilisateur
989
        id_filter, use_proxy = self._check_filter(ticket.service_url)
990
        ticket.filter = id_filter
991
        ticket.use_proxy = use_proxy
992
        # log.msg("%s -- %s : %s" % (session_id, _("Adding session for service"), ticket.service_url))
993
        # stockage du ticket
994
        self.app_sessions[appSessionID] = ticket
995
        self.user_app_tickets[session_id].append(ticket)
996
        # mise en place des timeouts de session
997
        ticket.timeout_callb = reactor.callLater(config.APP_TIMER, self.invalidate_app_ticket, appSessionID)
998
        return ticket
999

    
1000
    @trace
1001
    def invalidate_app_ticket(self, appSessionID):
1002
        """invalide un ticket d'application pour empêcher son utilisation
1003
        """
1004
        if appSessionID in self.app_sessions:
1005
            self.app_sessions[appSessionID].valid = False
1006

    
1007
    @trace
1008
    def verify_app_ticket(self, app_ticket, appurl):
1009
        """vérifie le ticket fourni par une application"""
1010
        verified = False
1011
        ticket = app_ticket
1012
        if app_ticket in self.app_sessions:
1013
            ticket = self.app_sessions[app_ticket]
1014
            verified = ticket.verif_ticket(app_ticket, appurl)
1015
        else:
1016
            # ticket inconnu, essai de vérification sur un serveur parent
1017
            if self._parent is not None:
1018
                try:
1019
                    return self._parent.verify_app_ticket(app_ticket, appurl)
1020
                except Exception, e:
1021
                    log.msg(_('Error calling function %s on parent server : %s') % ('verify_app_ticket', str(e)))
1022
                    pass
1023
        if not verified:
1024
            log.msg("! %s %s !" % (_("Session verification failed for service"), get_service_from_url(appurl)))
1025
        return verified, app_ticket
1026

    
1027
    @trace
1028
    def get_user_details(self, app_ticket, appurl, sections=False, renew=False, keep_valid=False):
1029
        """vérifie un ticket et renvoie les informations sur l'utilisateur
1030
        section : si True, on renvoie aussi une description de l'organisation des données
1031
        renew : si True, nécessite que l'utilisateur vienne de s'authentifier
1032
        keep_valid : si True, le ticket reste valide après lecture des informations
1033
        """
1034
        if config.DEBUG_LOG:
1035
            log.msg('--- %s %s' % (_("Validating session for"), appurl))
1036
        code = 'INVALID_TICKET'
1037
        detail = "%s : %s" % (_("Unknown ticket"), app_ticket)
1038
        if app_ticket in self.app_sessions:
1039
            if config.DEBUG_LOG:
1040
                log.msg('--- %s' % _("Session OK"))
1041
            ticket = self.app_sessions[app_ticket]
1042
            # si le paramètre renew a été envoyé et que le ticket provient d'une
1043
            # session SSO, on ne le valide pas
1044
            if not ticket.from_credentials and renew:
1045
                detail = _("Ticket %s was not delivered during authentication process") % app_ticket
1046
            elif ticket.verif_ticket(app_ticket, appurl, keep_valid):
1047
                if config.DEBUG_LOG:
1048
                    log.msg('--- %s' % _("Appurl verified"))
1049
                return True, self._get_user_details(ticket.session_id, ticket.filter, sections, ticket)
1050
            else:
1051
                code = 'INVALID_SERVICE'
1052
                detail = _("Ticket has not been delivered for service %s") % appurl
1053
                log.msg("! %s !" % _("Error reading user data : invalid session"))
1054
        else:
1055
            # session inconnue sur ce serveur, on essaie sur le serveur parent
1056
            if self._parent is not None:
1057
                # récupération des infos sur le serveur parent
1058
                # XXX  FIXME : re-filtrer les infos du serveur parent avec les filtres locaux ??
1059
                try:
1060
                    return self._parent.get_user_details(app_ticket, appurl, sections, renew)
1061
                except Exception, e:
1062
                    log.msg(_('Error calling function %s on parent server : %s') % ('get_user_details', str(e)))
1063
                    pass
1064
        log.msg("! %s %s !" % (_("User data access denied for service"), appurl))
1065
        if sections:
1066
            return False, ({'code':code,'detail':detail},"")
1067
        return False, {'code':code,'detail':detail}
1068

    
1069
    @trace
1070
    def get_proxy_granting_ticket(self, app_ticket, service, pgturl):
1071
        # si une url de proxy est donnée, on crée un ticket TGC
1072
        # la conformité de l'url est vérifiée lors de la création.
1073
        if app_ticket in self.app_sessions:
1074
            ticket = self.app_sessions[app_ticket]
1075
            if ticket.verif_ticket(app_ticket, service, keep_valid=True):
1076
                ticket.generate_pgt(pgturl)
1077
                self.proxy_granting_sessions[ticket.pgt] = ticket
1078
                return ticket
1079
        return None
1080

    
1081
    @trace
1082
    def get_proxy_ticket(self, pgt, target_service):
1083
        # récupération de la session
1084
        app_ticket = self.proxy_granting_sessions[pgt]
1085
        appSessionID = gen_ticket_id('PT', self.address)
1086
        # cas spécial : imapproxy ne fonctionne pas avec des pwd de + de 61 caractères
1087
        if target_service.startswith('imap://') or target_service.startswith('imaps://'):
1088
            appSessionID = appSessionID[:60]
1089
        ticket = self.init_app_session(appSessionID, app_ticket.session_id, target_service, app_ticket.from_credentials)
1090
        log.msg("%s -- %s %s" % (app_ticket.session_id, _("Proxy Session created for service"), target_service))
1091
        ticket.parent = app_ticket
1092
        return ticket.ticket
1093

    
1094
    @trace
1095
    def invalidate_proxy(self, app_ticket):
1096
        """Supprime les paramètres de proxy d'un app_ticket si les vérifications sur l'url de callback ont échoué
1097
        """
1098
        if app_ticket in self.app_sessions:
1099
            ticket = self.app_sessions[app_ticket]
1100
            if ticket.pgt is not None:
1101
                del self.proxy_granting_sessions[ticket.pgt]
1102
            ticket.reset_pgt()
1103

    
1104
    def get_app_infos(self, appurl):
1105
        """renvoie les information définies pour une url
1106
        """
1107
        url = urlparse.urlparse(appurl)
1108
        if url.scheme in self.apps:
1109
            store = self.apps[url.scheme]
1110
        else:
1111
            store = self.apps['http']
1112
        # recherche d'une application avec le même port/chemin
1113
        if url.port in store:
1114
            apps = store[url.port]
1115
        else:
1116
            # si pas de port correspondant, on regarde dans les applis sans port spécifique
1117
            apps = store['all']
1118
        # gestion du path, il faut que le path commence par ce qu'on veut
1119
        # on trie les chemins connus pour commencer par le plus spécifique
1120
        app_paths = apps.keys()
1121
        app_paths.sort(reverse=True)
1122
        url_path = url.path
1123
        if not url_path.startswith('/'): url_path = '/' + url_path
1124
        for baseurl in app_paths:
1125
            if not baseurl.startswith('/'): check_baseurl = '/' + baseurl
1126
            check_baseurl = baseurl
1127
            if url_path.startswith(check_baseurl):
1128
                for app in apps[baseurl]:
1129
                    scheme, addr, typeaddr, id_filter, use_proxy = app
1130
                    if typeaddr == 'ip':
1131
                        addr_ok = check_hostname_by_ip(url, addr)
1132
                    else:
1133
                        addr_ok = check_hostname_by_domain(url, addr)
1134
                    if addr_ok:
1135
                        # on a une appli correspondant à l'url, on renvoie le filtre associé
1136
                        if config.DEBUG_LOG:
1137
                            log.msg(_("Applied filter %s for url %s") % (id_filter, get_service_from_url(appurl)))
1138
                        return id_filter, use_proxy
1139
                        break
1140
        return None
1141

    
1142
    @trace
1143
    def _check_filter(self, appurl):
1144
        """vérifie si un filtre est disponible pour l'url"""
1145
        return self.get_app_infos(appurl) or ("default", "")
1146

    
1147
    @trace
1148
    def _filter_data(self, infos, id_filter, sections, ticket=None):
1149
        """filtrage en fonction de l'application si des filtres sont définis"""
1150
        data = {}
1151
        filter_data = {}
1152
        if id_filter in self.filters:
1153
            # si des attributs globaux sont définis, on les prend en compte
1154
            # (pas dans le cas d'un ticket à destination du protocole saml)
1155
            if ticket and not hasattr(ticket, 'saml_ident'):
1156
                for section, glob_attrs in self.global_filter.items():
1157
                    for libelle_cas, nom_val in glob_attrs.items():
1158
                        data[nom_val] = infos.get(nom_val, '')
1159
                    if section not in filter_data:
1160
                        filter_data[section] = copy.copy(glob_attrs)
1161
                    else:
1162
                        filter_data[section].update(copy.copy(glob_attrs))
1163
            for section, attrs in self.filters[id_filter].items():
1164
                for libelle_cas, nom_val in attrs.items():
1165
                    data[nom_val] = infos.get(nom_val, '')
1166
                if section in filter_data:
1167
                    filter_data[section].update(attrs)
1168
                else:
1169
                    filter_data[section] = attrs
1170
        else:
1171
            # définir les données à renvoyer par défaut !
1172
            data.update(infos)
1173
            filter_data = ""
1174
        if sections:
1175
            return data, filter_data
1176
        else:
1177
            return data
1178

    
1179
    def _add_user_infos(self, infos, session_id, ticket):
1180
        # ajout des informations statiques
1181
        infos.update({'rne':[config.RNE],'nom_etab':[config.ETABLISSEMENT]})
1182
        # dans le cas d'un ticket SAML, on passe également le nom de l'entité
1183
        # à joindre ainsi que son rôle (IDPSSODescriptor ou SPSSODescriptor)
1184
        if ticket and hasattr(ticket, 'saml_ident'):
1185
            infos['saml_ident'] = ticket.saml_ident
1186
        if ticket and hasattr(ticket, 'saml_role'):
1187
            infos['saml_role'] = ticket.saml_role
1188
        if ticket and hasattr(ticket, 'uaj'):
1189
            infos['uaj'] = ticket.uaj
1190
        # si le ticket d'application est connu, on passe également le service à atteindre dans les attributs
1191
        if ticket and hasattr(ticket, 'service_url'):
1192
            infos['service_url'] = ticket.service_url
1193
        try:
1194
            # information sur le niveau de sécurité de la session si disponible (actuellement: mot de passe ou OTP)
1195
            auth_class = self.user_sessions[session_id][2]
1196
            assert auth_class in available_contexts.values()
1197
        except:
1198
            # si pas d'infos, on considère que c'est une authentification par mot de passe (https)
1199
            auth_class = available_contexts['URN_PROTECTED_PASSWORD']
1200
        infos['auth_class'] = auth_class
1201

    
1202
    @trace
1203
    def _get_user_details(self, session_id, id_filter, sections, ticket=None):
1204
        """Renvoie les informations connues de l'utilisateur lié à <session_id>"""
1205
        # on récupère l'id de l'utilisateur
1206
        # user_id = self.user_sessions[session_id]
1207
        if self.check_ticket_issuer(session_id):
1208
            infos = self.data_cache[session_id]
1209
        else:
1210
            # session distante
1211
            try:
1212
                infos = self._parent.get_user_info(session_id, True)
1213
            except Exception, e:
1214
                log.msg(_('Error calling function %s on parent server : %s') % ('get_user_info', str(e)))
1215
                infos = {}
1216
        # ajout d'informations supplémentaires liées au contexte
1217
        self._add_user_infos(infos, session_id, ticket)
1218
        # dictionnaire des valeurs calculées dont les fonctions renvoient plusieurs attributs
1219
        # ces information sont ajoutées en dernier et écrasent les valeurs des fonctions 'basiques'
1220
        # attention : si plusieurs de ces fonctions renvoient les même attributs, la priorité sera définie
1221
        # par l'ordre des noms de fichier (à préfixer par exemple avec un numéro).
1222
        multi_infos = {}
1223
        # lancement des fonctions de calcul de valeurs supplémentaires
1224
        calc_names = self.user_infos.dict_infos.keys()
1225
        calc_names.sort()
1226
        # on trie par ordre alphabetique
1227
        calc_start_time = timeit.default_timer()
1228
        for calc_name in calc_names:
1229
            calc_func, use_cache = self.user_infos.dict_infos[calc_name]
1230
            if use_cache and calc_name in self.calc_cache.get(session_id, {}):
1231
                calc_infos = self.calc_cache[session_id][calc_name]
1232
                if config.DEBUG_LOG:
1233
                    log.msg(_('%s --- calculated attributes (%s) fetched from cache') % (session_id, calc_name))
1234
            else:
1235
                # execution de la fonction de calcul, sauf pour les fonctions
1236
                # définies comme utilisant un cache en cas d'appels successifs
1237
                try:
1238
                    known_infos = {}
1239
                    known_infos.update(infos)
1240
                    known_infos.update(multi_infos)
1241
                    calc_infos = calc_func(known_infos)
1242
                    # les fonctions doivent toujours renvoyer des valeurs
1243
                    # ou au pire un dictionnaire vide (attributs multiples)
1244
                    assert calc_infos is not None, _('calculated attribute returned None')
1245
                    if use_cache:
1246
                        self.calc_cache[session_id][calc_name] = calc_infos
1247
                except Exception, e:
1248
                    log.msg("""! %s '%s' :  %s""" % (_("Error computing data for attribute"), calc_name, str(e)))
1249
                    if config.DEBUG_LOG:
1250
                        traceback.print_exc()
1251
                if config.DEBUG_LOG:
1252
                    if use_cache:
1253
                        log.msg(_('%s --- calculated attributes (%s) processed and stored in cache') % (session_id, calc_name))
1254
                    else:
1255
                        log.msg(_('%s --- calculated attributes processed (%s)') % (session_id, calc_name))
1256
            if type(calc_infos) is dict:
1257
                # la fonction de calcul renvoie plusieurs attributs
1258
                # on les stocke comme attributs 'multiples'
1259
                multi_infos.update(calc_infos)
1260
            else:
1261
                infos[calc_name] = calc_infos
1262
        log.msg('%s --- durée de calcul des attributs : %.1f' % (session_id, timeit.default_timer() - calc_start_time))
1263
        # on ajoute les attributs 'multiple'
1264
        infos.update(multi_infos)
1265
        infos = self._filter_data(infos, id_filter, sections, ticket)
1266
        if config.DEBUG_LOG:
1267
            log.msg('--- %s' % _("User data sent"))
1268
        return infos