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

###########################################################################
#
# Eole NG - 2007
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
# Licence CeCill  cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# authserver.py
#
# serveur xmlrpc de gestion de session SSO Eole
#
###########################################################################
import os, shutil, time, traceback, urllib2, copy
import xmlrpclib
from glob import glob
# imports twisted
from twisted.internet import reactor, defer
# imports sso eole
import config
import timeit

# yes SessionManager is hard to disentangle, this is a hard standalone version
UNITTESTS_MODE = False
if UNITTESTS_MODE:
    _ = str
    def trace(func):
        return func
    from twisted.python import log
    default_idp_options = {}
    available_contexts = {}
    available_contexts['URN_PROTECTED_PASSWORD'] = 'urn_psswd'
    available_contexts['URN_TIME_SYNC_TOKEN'] = "urn_time_sync"
    #config.USE_SECURID = True
else:
    from page import trace, log
    #import de page.py pour pouvoir recharger les templates dans reload
    import page
    from saml_utils import get_metadata, date_from_string
    from saml_crypto import recreate_cert
    from saml_utils import available_contexts, default_idp_options

from eolesso.dataproxy import LDAPProxy
from eolesso.ticket import AppTicket
from eolesso.util import *
from eolesso.ticketcache import InvalidSession, TicketCache, SamlMsgCache
# import des fonctions de calcul de données utilisateur
import user_infos
# import des attributs de fédération externes
import external_attrs

if config.USE_SECURID or config.SECURID_PROVIDER:
    from securid_utils import SecuridUserStore

class SSOSessionManager(object):

    def __init__(self):
        self.address = config.AUTH_SERVER_ADDR
        self.init_caches()
        self._parent = None
        self.parent_url = None
        self._init_conf()

    def reload_conf(self):
        reload(user_infos)
        reload(external_attrs)
        reload(config)
        reload(page)
        self._init_conf()
        self.load_conf(reloading=True)
        return True

    def init_caches(self):
        # cache des "login tickets" (pour empêcher les navigateurs de 'rejouer' l'authentification avec un cache)
        self.login_sessions = TicketCache(180, self.address, 'LT')
        # cache des "granting tickets (ticket de session sso)"
        self.user_sessions = {}
        # cache des attributs utilisateur
        self.data_cache = {}
        # cache des attributs utilisateur (fonctions calculées non dynamiques)
        self.calc_cache = {}
        # cache des "application" et "proxy" tickets
        self.app_sessions = {}
        # dictionnaire d'association session utilisateur / tickets
        self.user_app_tickets = {}
        # cache pour la gestion des sessions de proxys (expirent en même temps que la session utilisateur)
        self.proxy_granting_sessions = {}
        # cache pour la gestion des sessions de fédération (fournisseur de service)
        self.saml_sessions = {}
        # caches de vérification des derniers messages envoyés/traités
        self.saml_sent_msg = SamlMsgCache(500)
        self.saml_rcved_msg = SamlMsgCache(500)
        # informations transitoires sur les sessions en cours de traitement
        self.relay_state_cache = SamlMsgCache(1000)
        # cache des checkers PAM (Securid)
        self.checkers = {}
        # cache pour la gestion du single logout (logouts en cours)
        # dans le cadre d'une requête de déconnexion globale: stockage de la session sso et du ticket de l'appelant
        # format : {'id_session': ( état_succes_saml,
        #                           [tickets_saml_deco_confirmée],
        #                           (ticket_appelant, id_req_logout, entite_saml_appelante),
        #                           {id_reponse_saml:ticket},
        #                           [services_cas_appelés])
        #          }
        self.pending_logout = {}
        # contextes d'authentification gérés (SAML 2)
        self.allowed_contexts = available_contexts

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

    def _init_conf(self):
        self.filters = {}
        self.apps = {'http':{'all':{}},
                     'https':{'all':{}},
                     'all':{'all':{}},
                     'sp_ident':{}}
        self.sp_meta = {}
        self.req_attributes = {}
        self.opt_attributes = {}
        self.attribute_sets = {}
        self.attribute_sets_ids = {}
        self.associations = {}
        static_data = {'rne': [config.RNE], 'nom_etab': [config.ETABLISSEMENT]}
        proxy = LDAPProxy(config.LDAP_SERVER, config.LDAP_PORT,
                          config.LDAP_BASE, config.LDAP_LABEL, config.LDAP_INFOS,
                          config.LDAP_READER, config.LDAP_READER_PASSFILE,
                          config.LDAP_LOGIN_OTP, static_data,
                          config.LDAP_MATCH_ATTRIBUTE)
        # lancement du serveur
        if config.PARENT_URL not in ('', None):
            self.parent_url = config.PARENT_URL
            self._parent = xmlrpclib.ServerProxy('%s/xmlrpc' % self.parent_url)
            log.msg("- %s : %s" % (_("Parent server defined"), self.parent_url))
        # proxy d'authentification
        self._data_proxy = proxy

    def load_conf(self, reloading=False):
        """Charge la configuration du serveur"""
        if not reloading:
            log.msg('* {0}'.format(_('loading server configuration')))
        # attributs calculés
        self.user_infos = user_infos
        log.msg("- %s : %s" % (_("Calculated Attributes defined"), ", ".join(user_infos.dict_infos.keys())))
        # attributs de fédération externes
        self.external_attrs = external_attrs.external_attrs
        log.msg("- %s : %s" % (_("External federation Attributes defined"), ", ".join(self.external_attrs.keys())))
        self.load_filters()
        # chargement des metadata SAML et jeux d'attributs
        self.load_attribute_sets()
        self.init_metadata()
        # informations sur les homonymes
        self.gen_homonymes_infos()
        # informations sur les établissements (etabs.js)
        update_etab_js(os.path.join(config.SSO_PATH,'interface','scripts','etabs.js'))
        if not reloading:
            # vérification de la validité des éventuels templates OTP
            if config.USE_SECURID or config.SECURID_PROVIDER:
                from securid_utils import get_otp_templates
                get_otp_templates(load = False)

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

    def end_session(self, session_id):
        """termine la session d'un utilisateur
        """
        # supression des différents tickets associés
        if session_id in self.user_sessions:
            # recherche des tickets associées à la session
            for ticket in self.user_app_tickets[session_id]:
                # recherche des éventuels PGT associés à cet AppTicket
                if ticket.pgt != None:
                    del self.proxy_granting_sessions[ticket.pgt]
                # recherche des éventuels sessions distantes (saml : idp distant) associées
                if hasattr(ticket,'idp_session_index'):
                    if ticket.idp_session_index in self.saml_sessions:
                        del self.saml_sessions[ticket.idp_session_index]
                # self.user_app_tickets[session_id].remove(ticket)
                del self.app_sessions[ticket.ticket]
            if session_id in self.user_app_tickets:
                del self.user_app_tickets[session_id]
            if session_id in self.data_cache:
                del self.data_cache[session_id]
            if session_id in self.calc_cache:
                del self.calc_cache[session_id]
            if session_id in self.pending_logout:
                del self.pending_logout[session_id]
            del self.user_sessions[session_id]
            log.msg("%s -- %s" % (session_id, _("Terminating session")))

    def gen_saml_id(self, data={}, prefix='_'):
        msg_id = gen_random_id(prefix)
        while self.saml_sent_msg.get_msg(msg_id) != None:
            msg_id = gen_random_id(prefix)
        data['date_creation']=time.time()
        self.saml_sent_msg.add(msg_id, data)
        return msg_id

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

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

    def replayed_saml_msg(self, msg_id):
        if msg_id in self.saml_rcved_msg:
            return True
        self.saml_rcved_msg.add(msg_id)
        return False

    def gen_relay_state(self, data, prefix='RS_'):
        msg_id = gen_random_id(prefix)
        while self.saml_sent_msg.get_msg(msg_id) != None:
            msg_id = gen_random_id(prefix)
        self.relay_state_cache.add(msg_id, data)
        return msg_id

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

    @trace
    def remove_old_ticket(self, session_id, saml_ident):
        """ deletes any existing session for a given partner entity
        """
        for ticket in self.user_app_tickets[session_id]:
            if getattr(ticket, 'saml_ident', '') == saml_ident:
                old_ticket = ticket.ticket
                # suppression du ticket des logout en cours si i len fait partie
                if session_id in self.pending_logout:
                    if old_ticket in self.pending_logout[session_id][1]:
                        self.pending_logout[session_id][1].remove(old_ticket)
                # suppression du ticket de la session utilisateur
                self.user_app_tickets[session_id].remove(ticket)
                # suppression des éventuelles indexs de session distants associés (authentification à travers un deuxième idp)
                if hasattr(ticket, 'idp_session_index'):
                    if ticket.idp_session_index in self.saml_sessions:
                        del(self.saml_sessions[ticket.idp_session_index])
                # suppression du ticket d'application
                del(self.app_sessions[old_ticket])
                del(ticket)
                break

    def load_filters(self):
        """chargement des filtres de données et applications
        """
        loaded_apps = []
        loaded_filters = []
        app_filters = set()
        # prise en compte des filtres globaux (attributs ajoutés à tous les filtres)
        glob_cfg = EoleParser()
        for conf_file in glob('%s/*.global' % config.FILTER_DIR):
            glob_cfg.read(conf_file)
        self.global_filter = dict((section, dict(glob_cfg.items(section))) for section in glob_cfg.sections())
        # filtres spécifiques à des applications et fournisseurs de services
        for conf_file in glob('%s/*.ini' % config.FILTER_DIR):
            conf_filename = os.path.splitext(os.path.basename(conf_file))[0]
            cfg = EoleParser()
            cfg.read(conf_file)
            if conf_filename.endswith('_apps') or conf_filename == 'applications':
                # liste des applications associées aux filtres
                new_apps, new_filters = self.load_applications(cfg)
                loaded_apps.extend(new_apps)
                app_filters.update(new_filters)
            else:
                # ajout d'un filtre de données
                app_filter = dict( (section, dict(cfg.items(section)))
                                   for section in cfg.sections() )
                self.filters[conf_filename] = app_filter
                loaded_filters.append(conf_filename)
        log.msg("- %s : %s" % (_("Filters loaded"), ", ".join(loaded_filters)))
        log.msg("- %s : %s" % (_("Applications defined"), ", ".join(loaded_apps)))
        # vérification de la présence de tous les filtres
        missing_filters = []
        for needed_filter in app_filters:
            if needed_filter not in self.filters:
                missing_filters.append(needed_filter)
        if missing_filters:
            log.msg('')
            log.msg('\t!! %s : %s' % (_('Following filters are needed but missing'), ', '.join(missing_filters)))
            log.msg('')

    def load_applications(self, cfg):
        """Chargement des applications associées aux filtres
        """
        loaded_apps = []
        filters = []
        for app in cfg.sections():
            try:
                app_infos = dict(cfg.items(app))
                # si un proxy particulier est déclaré, on vérifie qu'il est accessible
                use_proxy = app_infos.get('proxy', '')
                if ":" in use_proxy:
                    host, port = use_proxy.split(':')
                    if not check_url(host, port):
                        log.msg('\t! %s - %s : %s !' % (app, _('Warning, http proxy unreachable'), use_proxy))
                        use_proxy = ''
                # définition des filtres par entité de fournisseur de services (SAML)
                if 'sp_ident' in app_infos:
                    if 'sp_ident' in self.apps['sp_ident']:
                        log.msg(_("Warning, filter for service provider overwritten (already defined): %s") % str(app_infos['sp_ident']))
                    self.apps['sp_ident'][app_infos['sp_ident']] = (app_infos['filter'], use_proxy)
                    loaded_apps.append(app)
                else:
                    try:
                        port = app_infos['port'] = int(app_infos['port'])
                    except:
                        port = app_infos['port'] = 'all'
                    scheme = app_infos['scheme']
                    if scheme not in ['all', 'both']:
                        if scheme not in self.apps:
                            self.apps[scheme] = {'all':{}}
                        app_stores = [self.apps[scheme]]
                    elif scheme == 'both':
                        app_stores = [self.apps['http'], self.apps['https']]
                    else:
                        # pas de protocole spécifié, on stocke dans tous les protocoles
                        app_stores = self.apps.values()
                    # on trie les applications par port et chemin
                    for store in app_stores:
                        port_store = store.setdefault(port, {})
                        if app_infos['baseurl'] in port_store:
                            stored = False
                            for app_st in port_store[app_infos['baseurl']]:
                                if app_st[1] == app_infos['addr'] and app_st[2] == app_infos['typeaddr']:
                                    log.msg(_("Warning, application overwritten (already defined): %s") % (str(app_st)))
                                    app_st[3] = app_infos['filter']
                                    app_st[4] = use_proxy
                                    stored = True
                            if not stored:
                                port_store[app_infos['baseurl']].append([scheme, app_infos['addr'], app_infos['typeaddr'], app_infos['filter'], use_proxy])
                        else:
                            port_store[app_infos['baseurl']] = [[scheme,
                                                                 app_infos['addr'],
                                                                 app_infos['typeaddr'],
                                                                 app_infos['filter'],
                                                                 use_proxy]]
                        loaded_apps.append(app)
                        filters.append(app_infos['filter'])
            except Exception, e:
                log.msg("! %s %s : %s !" % (_("Error loading application"), app, e))
        return loaded_apps, filters

    def load_attribute_sets(self):
        """chargement des jeux d'attributs pour la fédération d'identité
        """
        self.check_eole_attr_sets()
        missing_indexes = []
        for conf_file in glob(os.path.join(config.ATTR_SET_DIR, '*.ini')):
            conf_filename = os.path.splitext(os.path.basename(conf_file))[0]
            if not conf_filename.startswith('associations'):
                cfg = EoleParser()
                cfg.read(conf_file)
                # ajout des jeux d'attributs (un seul par fichier)
                # on sépare les attributs en 2 parties:
                # user_attrs : attributs servant à créer le filtre de recherche de l'utilisateur
                # branch_attrs : attributs servant à déterminer un branche de recherche dans l'annuaire
                attr_set = {'user_attrs':{}, 'branch_attrs':{}, 'optional_attrs':{}}
                attr_set_index = None
                for section in cfg.sections():
                    # gestion de l'index du jeu d'attribut (si défini dans le fichier
                    if section == 'metadata':
                        try:
                            set_index = cfg.get(section, 'index')
                            assert int(set_index) >= 0
                            assert set_index not in self.attribute_sets_ids.values()
                            attr_set_index = set_index
                        except:
                            log.msg(_('invalid attribute set index for %s' ) % conf_filename)
                    elif section == 'branch_attrs':
                        attr_set[section] = dict(cfg.items(section))
                    elif section == 'optional':
                        for dist_attr, local_attr in cfg.items(section):
                            attr_set['optional_attrs'][dist_attr] = local_attr
                    else:
                        # tous les autres attributs sont considérés comme requis
                        for dist_attr, local_attr in cfg.items(section):
                            attr_set['user_attrs'][dist_attr] = local_attr
                # gestion de l'index du jeu d'attribut
                if attr_set_index is None:
                    self.attribute_sets_ids[conf_filename] = None
                    missing_indexes.append(conf_filename)
                else:
                    self.attribute_sets_ids[conf_filename] = attr_set_index
                self.attribute_sets[conf_filename] = attr_set
        # options par défaut
        self.associations['default'] = {}
        self.associations['default'].update(default_idp_options)
        for assoc_file in glob('%s/associations*.ini' % config.ATTR_SET_DIR):
            # chargement des associations <entité partenaire><set d'attributs>
            cfg = EoleParser()
            cfg.read(assoc_file)
            for entity_id in cfg.sections():
                assoc = self.associations.get(entity_id, {})
                # XXX FIXME : permettre d'associer un filtre ici au lieu des filtres avec saml_ident ?
                opts_path = {'attribute_set':config.ATTR_SET_DIR, 'filter':config.FILTER_DIR}
                for option, value in cfg.items(entity_id):
                    if option in ('attribute_set', 'filter'):
                        data_file = os.path.join(opts_path[option], '%s.ini' % value)
                        if not os.path.isfile(data_file):
                            log.msg('%s : %s %s' % (assoc_file, data_file, _('not found')))
                            continue
                    assoc[option] = value
                self.associations[entity_id] = assoc
                # Vérification sur les attributs non compatibles
                if is_true(assoc.get('passive', '')) and is_true(assoc.get('force_auth', '')):
                    log.msg('\t! %s : %s !' % (entity_id, _('Warning, force_auth and passive are mutually exclusive options, force_auth set to false')))
                    assoc['force_auth'] = 'false'
        for opt, val in self.associations['default'].items():
            if val != default_idp_options.get(opt, None):
                if opt == 'attribute_set':
                    log.msg(_('default attribute set redefined : %s') % val)
                else:
                    log.msg(_('default value for option %s defined to : %s') % (opt, val))
        # calcul et stockage des indexs manquants
        if missing_indexes:
            for set_id in range(len(self.attribute_sets)):
                set_id = str(set_id)
                if set_id not in self.attribute_sets_ids.values():
                    attr_set = missing_indexes.pop(0)
                    # attribution de l'index au premier set n'en ayant pas
                    self.store_set_index(set_id, attr_set)
                    self.attribute_sets_ids[attr_set] = set_id
                    if len(missing_indexes) == 0:
                        # tous les sets ont un index
                        break

    def store_set_index(self, index, setname):
        """enregistre l'index du jeu d'attribut dans le fichier de définition"""
        conf_file = os.path.join(config.ATTR_SET_DIR, "%s.ini" % setname)
        cfg = EoleParser()
        cfg.read(conf_file)
        if not cfg.has_section('metadata'):
            cfg.add_section('metadata')
        cfg.set('metadata', 'index', index)
        try:
            f_conf = open(conf_file, 'w')
            cfg.write(f_conf)
            f_conf.close()
            log.msg(_('index %s assigned to attribute set %s') % (index, setname))
        except IOError:
            log.msg(_('error updating configuration file %s') % conf_file)
            return False
        return True

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


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

    @trace
    def load_metadata(self, saml_ident):
        try:
            sp_meta = get_metadata(saml_ident)
            if 'entityID' in sp_meta:
                saml_ident = sp_meta['entityID']
            if type(saml_ident) == unicode:
                saml_ident = saml_ident.encode(config.encoding)
            f_cert = os.path.join(config.METADATA_DIR,'certs','%s.crt' % saml_ident.replace(os.sep,'_'))
            if not os.path.isfile(f_cert):
                # stockage des certificats sous forme de fichiers pem
                cert_data = None
                if 'SignCert' in sp_meta:
                    cert_data = sp_meta['SignCert']
                elif 'SignCert' in sp_meta.get('SPSSODescriptor',{}):
                    cert_data = sp_meta['SPSSODescriptor']['SignCert']
                elif 'SignCert' in sp_meta.get('IDPSSODescriptor',{}):
                    cert_data = sp_meta['IDPSSODescriptor']['SignCert']
                if cert_data:
                    cert_buffer = recreate_cert(cert_data)
                    # stockage du certificat de signature
                    f = open(f_cert, 'w')
                    f.write(cert_buffer)
                    f.close()
        except Exception, e:
            log.msg("- %s (%s)" % (_("Error fetching SAML metadata for %s : ") % saml_ident, str(e)))
            return {}
        if saml_ident not in self.sp_meta:
            attr_set = self.associations.get(saml_ident, {}).get('attribute_set', '')
            if attr_set not in ('', 'default'):
                attr_msg = ' (%s : %s)' % (_('attribute set'), attr_set)
            else:
                attr_msg = ''
            log.msg("- %s : %s%s" % (_("Partner entity initialized"), saml_ident, attr_msg))
        # mise à jour des métadonnées
        self.sp_meta[saml_ident] = sp_meta
        return sp_meta

    @trace
    def get_metadata(self, saml_ident):
        if saml_ident in self.sp_meta:
            return self.sp_meta[saml_ident]
        else:
            try:
                sp_meta = self.load_metadata(saml_ident)
            except:
                sp_meta = {}
        return sp_meta

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

    def gen_homonymes_infos(self):
        """Génère un fichier javascript contenant des
        informations à afficher dans le cas ou des homonymes sont détectés
        """
        h_dir = config.HOMONYMES_DIR
        h_script = os.path.join(config.SSO_PATH, 'interface', 'scripts', 'homonymes.js')
        script = """var msgs=new Array();\nvar host_infos=new Array();\n"""
        infos_used = {}
        host_infos = []
        index_msg = 1
        for host, infos in self._data_proxy.ldap_infos.items():
            if infos and os.path.isfile(os.path.join(h_dir, infos)):
                if infos not in infos_used:
                    msg = open(os.path.join(h_dir, infos)).read().strip().replace('"', "'")
                    infos_used[infos] = index_msg
                    script += """msgs['msg%s']="%s";\n""" % (str(index_msg), msg)
                    index_msg += 1
                host_infos.append("""host_infos['%s']="msg%s";""" % (host, infos_used[infos]))
        if host_infos:
            script += "\n".join(host_infos);
        f_script = open(h_script, 'w')
        f_script.write(script)
        f_script.close()

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

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

        Les options gérées sont:

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

    @trace
    def check_federation_allowed(self, idp_ident, idp_initiated=False):
        """vérifie qu'un fournisseur d'identité est autorisé à fournir des assertions
        idp_initiated : vérifie si les réponses non sollicitées sont autorisées (pas de requête préalable)
        """
        default_allow_idp = self.associations['default'].get('allow_idp', 'true')
        default_allow_idp_initiated = self.associations['default'].get('allow_idp_initiated', 'true')
        if idp_ident in self.associations:
            assoc_data = self.associations.get(idp_ident, {})
            if idp_initiated:
                # assertions spontanées autorisées par défaut si rien n'est spécifié
                if not is_true(assoc_data.get('allow_idp_initiated', default_allow_idp_initiated)):
                    return False
            # assertions autorisées par défaut si pas de mention contraire
            return is_true(assoc_data.get('allow_idp', default_allow_idp))
        # pas d'informations données, utilise les autorisations par défaut
        if is_true(default_allow_idp):
            if not idp_initiated or is_true(default_allow_idp_initiated):
                return True
        return False

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

    @trace
    def init_user_session(self, user_data, username, auth_instant , search_attrs=None, auth_class=available_contexts['URN_PROTECTED_PASSWORD'], idp_ident=None):
        sessionID = gen_ticket_id('TGC', self.address)
        if sessionID != "":
            # XXX FIXME : utilisation de la clé de fédération comme nom d'utilisateur CAS ?
            if search_attrs is not None:
                self.user_sessions[sessionID] = [search_attrs, auth_instant, auth_class, idp_ident]
            else:
                self.user_sessions[sessionID] = [username, auth_instant, auth_class, idp_ident]
            # stockage des données utilisateur
            for ignored_attr in config.IGNORED_ATTRS:
                if ignored_attr in user_data:
                    del(user_data[ignored_attr])
            self.data_cache[sessionID] = user_data
            self.calc_cache[sessionID] = {}
            # initialisation de la liste des tickets associés
            self.user_app_tickets[sessionID] = []
            reactor.callLater(config.SESSION_TIMER, self.end_session, sessionID)
        return sessionID

    @trace
    def authenticate_federated_user(self, attrs, idp_ident, auth_instant, auth_class, current_session=None, search_branch=None):
        # recherche du jeu d'attributs à utiliser
        user_data = {}
        optional_attrs = {}
        # création d'une copie du jeu d'attribut passé aux fonctions
        # de calcul d'attributs externes pour mise à jour
        attr_set = self.get_attribute_set(idp_ident, search_branch)
        user_attrs = {}
        user_attrs.update(attrs)
        idp_attrs = {}
        # si des attributs 'externes' sont présents,
        # on les traite avant de chercher les occurences LDAP
        for idp_attr, value in attrs.items():
            if idp_attr in self.external_attrs.keys():
                ext_attrs = self.external_attrs[idp_attr].get_local_attrs(copy.copy(value), attr_set)
                # on remplace l'attribut par ceux retournés
                idp_attrs.update(ext_attrs)
            else:
                idp_attrs[idp_attr] = value
        # recherche de l'utilisateur en fonction du jeu d'attributs
        if search_branch is None:
            search_branch = self._data_proxy.get_search_branch(idp_attrs, attr_set)
        if search_branch is None:
            log.msg(_("Federation - Unable to determine search branch with following attributes : "), idp_attrs)
        else:
            try:
                # si branche non retrouvée, on considère que la fédération a échoué
                search_attrs = {}
                search_base = None
                # on parcourt les attributs reçus
                for idp_attr, value in idp_attrs.items():
                    if idp_attr in attr_set['user_attrs']:
                        # correspond à la clé de fédération
                        if len(value) > 0:
                            search_attrs[attr_set['user_attrs'][idp_attr]] = value[0]
                    if idp_attr in attr_set['optional_attrs']:
                        # attributs supplémentaires, on les ajoutera
                        # au données utilisateur (écrasés si présents localement ?)
                        optional_attrs[attr_set['optional_attrs'][idp_attr]] = value
                # tous les attributs doivent correspondre à ceux demandés dans le set d'attributs
                if search_attrs and (len(search_attrs) == len(attr_set['user_attrs'])):
                    log.msg(_('Federation: searching user matching attributes %s (search branch: %s)') % (str(search_attrs), search_branch))
                    defer_fed_data = self._data_proxy.get_user_data(search_attrs, search_branch)
                    return defer_fed_data.addCallbacks(self.callb_federation, self.errb_auth,
                           callbackArgs=[search_attrs, optional_attrs, idp_ident, auth_instant, auth_class, current_session])
            except:
                traceback.print_exc()
                user_data = {}
        return defer.succeed(('', user_data))

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

    def get_user_log_data(self, user_data, additional_attrs=[]):
        log_attrs = []
        display_attrs = ['uid', 'cn']
        display_attrs.extend(additional_attrs)
        for log_attr in display_attrs:
            if log_attr in user_data:
                log_attrs.append("%s: %s" % (log_attr, user_data[log_attr][0]))
        return ", ".join(log_attrs)

    @trace
    def callb_federation(self, result, search_attrs, optional_attrs, idp_ident, auth_instant, auth_class, current_session):
        bind_success, user_data = result
        sessionID = ''
        if current_session:
            if self.user_sessions[current_session][0] == search_attrs:
                # session déjà existante pour cet utilisateur
                return current_session, user_data
        first_fed_key = search_attrs.keys()[0]
        if first_fed_key in user_data:
            # utilisateur local trouvé: création de la session
            username = user_data[first_fed_key][0]
            # si des attributs optionels sont présents, on  les intègre dans les attributs utilisateur
            # les attributs locaux écrasent les attributs reçus si ils sont présents
            if optional_attrs:
                optional_attrs.update(user_data)
                user_data = optional_attrs
            # description de l'utilisateur trouvé dans les logs
            sessionID = self.init_user_session(user_data, username, auth_instant, search_attrs, auth_class, idp_ident)
            log.msg(_('Federation: user found (%s). Session ID : %s') % (self.get_user_log_data(user_data, [first_fed_key]), sessionID))
        else:
            log.msg(_('Federation: user from %s not found (%s)') % (idp_ident, str(search_attrs)))
        return sessionID, user_data

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

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

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

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

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

    @trace
    def validate_session(self, session_id):
        """vérifie un session_id existant"""
        if self.check_ticket_issuer(session_id) and session_id in self.user_sessions:
            return True
        if self._parent is not None:
            try:
                valid_session = self._parent.validate_session(session_id)
            except Exception, e:
                log.msg(_('Error calling function %s on parent server : %s') % ('validate_session', str(e)))
                # XXX FIXME : différencier socket.error ?
                valid_session = False
            if valid_session:
                log.msg("%s : %s" % (session_id, _("User session validated on parent server")))
                # si la session n'est pas présente localement, mais validée par le parent, on l'ajoute (ex : redémarrage du serveur local)
                if not session_id in self.user_sessions:
                    self.user_sessions[session_id] = [None,None,None,None]
                    self.user_app_tickets[session_id] = []
            return valid_session
        log.msg("! %s : %s !" % (_("Invalid session"), session_id))
        return False

    @trace
    def verify_session_id(self, session_id):
        """vérifie un session_id"""
        if self.check_ticket_issuer(session_id) and session_id in self.user_sessions:
            return True, session_id
        if self._parent is not None:
            try:
                return self._parent.verify_session_id(session_id)
            except Exception, e:
                log.msg(_('Error calling function %s on parent server : %s') % ('verify_session', str(e)))
                return False, session_id
        log.msg("! %s : %s !" % (_("Unknown user session"), session_id))
        return False, session_id

    @trace
    def get_user_info(self, session_id, details=False):
        """méthode de convenance pour récupérer l'utilisateur"""
        if session_id in self.user_sessions:
            if self.check_ticket_issuer(session_id):
                if details:
                    return self._get_user_details(session_id, "", {})
                else:
                    return self.user_sessions[session_id][0]
            else:
                try:
                    # session du serveur parent
                    return self._parent.get_user_info(session_id, details)
                except:
                    pass
        raise InvalidSession(session_id)

    @trace
    def get_auth_instant(self, app_ticket):
        """renvoie la date à laquelle l'authentification a eu lieu
        """
        session_id = app_ticket
        ticket = app_ticket
        if app_ticket in self.app_sessions and self.app_sessions[app_ticket] is not None:
            ticket = self.app_sessions[app_ticket]
            session_id = ticket.session_id
            if session_id in self.user_sessions:
                return self.user_sessions[session_id][1]
        else:
            # ticket inconnu, essai de vérification sur un serveur parent
            if self._parent is not None:
                try:
                    return self._parent.get_auth_instant(app_ticket)
                except Exception, e:
                    log.msg(_('Error calling function %s on parent server : %s') % ('verify_app_ticket', str(e)))
                    pass
        raise InvalidSession(session_id)

    @trace
    def get_auth_class(self, app_ticket):
        """retourne la classe d'authentification d'une session"""
        session_id = app_ticket
        ticket = app_ticket
        if app_ticket in self.app_sessions and self.app_sessions[app_ticket] is not None:
            ticket = self.app_sessions[app_ticket]
            session_id = ticket.session_id
            if session_id in self.user_sessions:
                return self.user_sessions[session_id][2]
        if self._parent is not None:
            try:
                return self._parent.get_auth_class(app_ticket)
            except Exception, e:
                log.msg(_('Error calling function %s on parent server : %s') % ('get_auth_class', str(e)))
                return None
        log.msg("! %s : %s !" % (_("Unknown user session"), session_id))
        return None

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

    def logout(self, session_id):
        """vérifie un session_id"""
        if session_id in self.user_sessions:
            self.end_session(session_id)
            if not self.check_ticket_issuer(session_id):
                # session délivrée par le parent ?
                try:
                    return self._parent.logout(session_id)
                except Exception, e:
                    log.msg(_('Error calling function %s on parent server : %s') % ('logout', str(e)))
                    pass
            else:
                return 'ok'
        return ''

    @trace
    def get_app_ticket(self, session_id, appurl, ticket_prefix='ST', from_credentials=False, idp_ident=None):
        # si cette session utilisateur est valide, on crée un ticket pour l'application
        appSessionID = ''
        if self.validate_session(session_id):
            # génération d'un nouveau ticket d'application associé à la session
            appSessionID = gen_ticket_id(ticket_prefix, self.address)
            ticket = self.init_app_session(appSessionID, session_id, appurl, from_credentials)
            if ticket.filter != '':
                filter_msg = ' (%s : %s)' % (_("attribute filter"), ticket.filter)
            else:
                filter_msg = ''
            if idp_ident:
                # ticket d'information sur une fédération établie
                log.msg("%s -- %s" % (session_id, _("federation information stored (identity provider: %s)") % idp_ident))
            else:
                log.msg("%s -- %s %s%s" % (session_id, _("Session authorized for service"), ticket.service_url, filter_msg))
            return appSessionID
        else:
            log.msg("%s : %s" % (_("Unknown user session"), session_id))
        log.msg("! %s -- %s %s !" % (session_id, _("Failed to create session for service"), appurl))
        return ''

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

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

    @trace
    def verify_app_ticket(self, app_ticket, appurl):
        """vérifie le ticket fourni par une application"""
        verified = False
        ticket = app_ticket
        if app_ticket in self.app_sessions:
            ticket = self.app_sessions[app_ticket]
            verified = ticket.verif_ticket(app_ticket, appurl)
        else:
            # ticket inconnu, essai de vérification sur un serveur parent
            if self._parent is not None:
                try:
                    return self._parent.verify_app_ticket(app_ticket, appurl)
                except Exception, e:
                    log.msg(_('Error calling function %s on parent server : %s') % ('verify_app_ticket', str(e)))
                    pass
        if not verified:
            log.msg("! %s %s !" % (_("Session verification failed for service"), get_service_from_url(appurl)))
        return verified, app_ticket

    @trace
    def get_user_details(self, app_ticket, appurl, sections=False, renew=False, keep_valid=False):
        """vérifie un ticket et renvoie les informations sur l'utilisateur
        section : si True, on renvoie aussi une description de l'organisation des données
        renew : si True, nécessite que l'utilisateur vienne de s'authentifier
        keep_valid : si True, le ticket reste valide après lecture des informations
        """
        if config.DEBUG_LOG:
            log.msg('--- %s %s' % (_("Validating session for"), appurl))
        code = 'INVALID_TICKET'
        detail = "%s : %s" % (_("Unknown ticket"), app_ticket)
        if app_ticket in self.app_sessions:
            if config.DEBUG_LOG:
                log.msg('--- %s' % _("Session OK"))
            ticket = self.app_sessions[app_ticket]
            # si le paramètre renew a été envoyé et que le ticket provient d'une
            # session SSO, on ne le valide pas
            if not ticket.from_credentials and renew:
                detail = _("Ticket %s was not delivered during authentication process") % app_ticket
            elif ticket.verif_ticket(app_ticket, appurl, keep_valid):
                if config.DEBUG_LOG:
                    log.msg('--- %s' % _("Appurl verified"))
                return True, self._get_user_details(ticket.session_id, ticket.filter, sections, ticket)
            else:
                code = 'INVALID_SERVICE'
                detail = _("Ticket has not been delivered for service %s") % appurl
                log.msg("! %s !" % _("Error reading user data : invalid session"))
        else:
            # session inconnue sur ce serveur, on essaie sur le serveur parent
            if self._parent is not None:
                # récupération des infos sur le serveur parent
                # XXX  FIXME : re-filtrer les infos du serveur parent avec les filtres locaux ??
                try:
                    return self._parent.get_user_details(app_ticket, appurl, sections, renew)
                except Exception, e:
                    log.msg(_('Error calling function %s on parent server : %s') % ('get_user_details', str(e)))
                    pass
        log.msg("! %s %s !" % (_("User data access denied for service"), appurl))
        if sections:
            return False, ({'code':code,'detail':detail},"")
        return False, {'code':code,'detail':detail}

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

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

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

    def get_app_infos(self, appurl):
        """renvoie les information définies pour une url
        """
        url = urlparse.urlparse(appurl)
        if url.scheme in self.apps:
            store = self.apps[url.scheme]
        else:
            store = self.apps['http']
        # recherche d'une application avec le même port/chemin
        if url.port in store:
            apps = store[url.port]
        else:
            # si pas de port correspondant, on regarde dans les applis sans port spécifique
            apps = store['all']
        # gestion du path, il faut que le path commence par ce qu'on veut
        # on trie les chemins connus pour commencer par le plus spécifique
        app_paths = apps.keys()
        app_paths.sort(reverse=True)
        url_path = url.path
        if not url_path.startswith('/'): url_path = '/' + url_path
        for baseurl in app_paths:
            if not baseurl.startswith('/'): check_baseurl = '/' + baseurl
            check_baseurl = baseurl
            if url_path.startswith(check_baseurl):
                for app in apps[baseurl]:
                    scheme, addr, typeaddr, id_filter, use_proxy = app
                    if typeaddr == 'ip':
                        addr_ok = check_hostname_by_ip(url, addr)
                    else:
                        addr_ok = check_hostname_by_domain(url, addr)
                    if addr_ok:
                        # on a une appli correspondant à l'url, on renvoie le filtre associé
                        if config.DEBUG_LOG:
                            log.msg(_("Applied filter %s for url %s") % (id_filter, get_service_from_url(appurl)))
                        return id_filter, use_proxy
                        break
        return None

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

    @trace
    def _filter_data(self, infos, id_filter, sections, ticket=None):
        """filtrage en fonction de l'application si des filtres sont définis"""
        data = {}
        filter_data = {}
        if id_filter in self.filters:
            # si des attributs globaux sont définis, on les prend en compte
            # (pas dans le cas d'un ticket à destination du protocole saml)
            if ticket and not hasattr(ticket, 'saml_ident'):
                for section, glob_attrs in self.global_filter.items():
                    for libelle_cas, nom_val in glob_attrs.items():
                        data[nom_val] = infos.get(nom_val, '')
                    if section not in filter_data:
                        filter_data[section] = copy.copy(glob_attrs)
                    else:
                        filter_data[section].update(copy.copy(glob_attrs))
            for section, attrs in self.filters[id_filter].items():
                for libelle_cas, nom_val in attrs.items():
                    data[nom_val] = infos.get(nom_val, '')
                if section in filter_data:
                    filter_data[section].update(attrs)
                else:
                    filter_data[section] = attrs
        else:
            # définir les données à renvoyer par défaut !
            data.update(infos)
            filter_data = ""
        if sections:
            return data, filter_data
        else:
            return data

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

    @trace
    def _get_user_details(self, session_id, id_filter, sections, ticket=None):
        """Renvoie les informations connues de l'utilisateur lié à <session_id>"""
        # on récupère l'id de l'utilisateur
        # user_id = self.user_sessions[session_id]
        if self.check_ticket_issuer(session_id):
            infos = self.data_cache[session_id]
        else:
            # session distante
            try:
                infos = self._parent.get_user_info(session_id, True)
            except Exception, e:
                log.msg(_('Error calling function %s on parent server : %s') % ('get_user_info', str(e)))
                infos = {}
        # ajout d'informations supplémentaires liées au contexte
        self._add_user_infos(infos, session_id, ticket)
        # dictionnaire des valeurs calculées dont les fonctions renvoient plusieurs attributs
        # ces information sont ajoutées en dernier et écrasent les valeurs des fonctions 'basiques'
        # attention : si plusieurs de ces fonctions renvoient les même attributs, la priorité sera définie
        # par l'ordre des noms de fichier (à préfixer par exemple avec un numéro).
        multi_infos = {}
        # lancement des fonctions de calcul de valeurs supplémentaires
        calc_names = self.user_infos.dict_infos.keys()
        calc_names.sort()
        # on trie par ordre alphabetique
        calc_start_time = timeit.default_timer()
        for calc_name in calc_names:
            calc_func, use_cache = self.user_infos.dict_infos[calc_name]
            if use_cache and calc_name in self.calc_cache.get(session_id, {}):
                calc_infos = self.calc_cache[session_id][calc_name]
                if config.DEBUG_LOG:
                    log.msg(_('%s --- calculated attributes (%s) fetched from cache') % (session_id, calc_name))
            else:
                # execution de la fonction de calcul, sauf pour les fonctions
                # définies comme utilisant un cache en cas d'appels successifs
                try:
                    known_infos = {}
                    known_infos.update(infos)
                    known_infos.update(multi_infos)
                    calc_infos = calc_func(known_infos)
                    # les fonctions doivent toujours renvoyer des valeurs
                    # ou au pire un dictionnaire vide (attributs multiples)
                    assert calc_infos is not None, _('calculated attribute returned None')
                    if use_cache:
                        self.calc_cache[session_id][calc_name] = calc_infos
                except Exception, e:
                    log.msg("""! %s '%s' :  %s""" % (_("Error computing data for attribute"), calc_name, str(e)))
                    if config.DEBUG_LOG:
                        traceback.print_exc()
                if config.DEBUG_LOG:
                    if use_cache:
                        log.msg(_('%s --- calculated attributes (%s) processed and stored in cache') % (session_id, calc_name))
                    else:
                        log.msg(_('%s --- calculated attributes processed (%s)') % (session_id, calc_name))
            if type(calc_infos) is dict:
                # la fonction de calcul renvoie plusieurs attributs
                # on les stocke comme attributs 'multiples'
                multi_infos.update(calc_infos)
            else:
                infos[calc_name] = calc_infos
        log.msg('%s --- durée de calcul des attributs : %.1f' % (session_id, timeit.default_timer() - calc_start_time))
        # on ajoute les attributs 'multiple'
        infos.update(multi_infos)
        infos = self._filter_data(infos, id_filter, sections, ticket)
        if config.DEBUG_LOG:
            log.msg('--- %s' % _("User data sent"))
        return infos
