Projet

Général

Profil

saml_resources.py

Bruno Boiget, 28/11/2016 11:07

Télécharger (64,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
# saml_resources.py
12
#
13
# support (limité) du protocole SAML 2.0 pour eole-sso
14
#
15
###########################################################################
16

    
17
import traceback
18
import urllib, os, socket, time
19

    
20
from twisted.web2 import http, responsecode
21
from twisted.web2.http_headers import Cookie as TwCookie, MimeType
22
from twisted.web2.resource import PostableResource
23
from twisted.internet.threads import callMultipleInThread
24
from twisted.python import failure
25

    
26
from pyeole import httprequest
27
from eolesso.util import (getCookies, gen_random_id, RedirectResponse,
28
                          format_err, get_service_from_url, is_true, http_headers)
29

    
30
import saml_message
31
from saml_crypto import sign_request
32
from saml_utils import (encode_request, gen_metadata, get_endpoint,
33
                        get_attributes, InternalError, Redirect,
34
                        extract_message, check_required_contexts,
35
                        process_auth_request, process_assertion,
36
                        encode_idp_cookie, split_form_arg)
37
from saml2 import samlp
38
from oidc_utils import openid_logout_url
39
from page import gen_page, trace, log
40
from config import (CERTFILE, AUTH_FORM_URL, IDP_IDENTITY, DEFAULT_CSS,
41
                    SEND_CAS_LOGOUT, DISPLAY_FEDERATION, DEBUG_LOG, encoding)
42

    
43
class SamlResource(PostableResource):
44

    
45
    content_type = 'text/html; charset=utf-8'
46
    # headers par défaut
47
    headers = http_headers
48

    
49
    def set_headers(self, resp, user_headers={}):
50
        """attache à la réponse HTTP les headers par défaut
51
        + les éventuels headers passés en paramètre
52
        """
53
        if self.content_type:
54
            resp.headers.setRawHeaders('Content-type', [self.content_type])
55
        for headers in (self.headers, user_headers):
56
            for h_name, h_values in headers.items():
57
                resp.headers.setRawHeaders(h_name, h_values)
58
        return resp
59

    
60
    def render(self, request):
61
        # errheaders = {'Content-type': MimeType.fromString('application/xml')}
62
        try:
63
            return self._render(request)
64
        except Redirect, exc:
65
            headers = {'Content-type': MimeType.fromString(self.content_type)}
66
            location = exc.location
67
            headers['location'] =  location
68
            log.msg(_('Redirecting to '), location)
69
            # 303 See other
70
            return http.Response(code = 303, headers = headers)
71
        except Exception, exc: # unknown error
72
            traceback.print_exc()
73
            return http.StatusResponse(code = responsecode.INTERNAL_SERVER_ERROR, description = str(exc))
74

    
75
    def _render(self):
76
        pass
77

    
78
def gen_page_err(err_msg, from_url, css):
79
    """construit une réponse d'erreur
80
    """
81
    form = format_err(err_msg)
82
    if from_url:
83
        form += """<p><FORM action="%s">
84
        <b>%s</b> : <br/><h2><font color="blue">%s</font></h2><br/></p>
85
        <p class="formvalidation"><input class="btn" type="submit" value ="%s"/></p>
86
        </form>""" % (from_url, _('Following page has been proposed'), from_url, _('Proceed to above url'))
87
    else:
88
        form += """<br/><form action="/login" method="post">
89
        <p class="formvalidation">
90
        <input class="btn" type="submit" value="%s"></p></form>""" % _('New session')
91
    return http.Response(stream = gen_page(_('Logout'), form, css))
92

    
93
class SamlLogout(SamlResource):
94
    """Resource de gestion du logout (logout cas + single logout saml)
95
    """
96
    addSlash = False
97

    
98
    def __init__(self, manager):
99
        self.manager = manager
100
        super(SamlLogout, self).__init__()
101

    
102
    @trace
103
    def saml_logout_request(self, ticket, sso_session, from_url, css):
104
        """Envoi d'une requête de déconnexion à une entité partenaire et stocke les informations sur l'origine de la déconnexion"""
105
        saml_identity = ticket.saml_ident
106
        saml_role = ticket.saml_role
107
        # Lecture des métadonnées du partenaire
108
        sp_meta = self.manager.get_metadata(saml_identity)
109
        user_id = ticket.name_id
110
        try:
111
            binding, service_url, response_url = get_endpoint(sp_meta, 'SingleLogoutService', ticket.saml_role)
112
        except InternalError:
113
            log.msg(_('no usable endpoint for {0}').format(sp_meta['entityID']))
114
            # store error for display ?
115
            return None
116

    
117
        response_id = self.manager.gen_saml_id({'type':'LogoutRequest'})
118
        if hasattr(ticket, 'idp_session_index'):
119
            session_index = ticket.idp_session_index
120
        else:
121
            session_index = ticket.ticket
122
        # Envoi de la demande de logout à l'entité correspondante
123
        if binding == samlp.BINDING_HTTP_POST:
124
            sign_method, req = saml_message.gen_logout_request(response_id, session_index, \
125
                    user_id, IDP_IDENTITY, sp_meta['entityID'], service_url, CERTFILE)
126
            # réponse utilisant le binding POST
127
            req = encode_request(req)
128
            form = """
129
                    <FORM action="%s" method="POST">
130
                    <input type="hidden" name="SAMLRequest" value="%s">
131
                    <input type="hidden" name="SigAlg" value="%s">
132
                    <input type="hidden" name="RelayState" value="%s">
133
                    <p class="formvalidation">
134
                    <input class="btn" type="submit" value="%s"></p>
135
            """ % (service_url, split_form_arg(req), sign_method, from_url, _("Proceed to service"))
136
            resp = http.Response(stream = gen_page(_('Logout request to service'), form, css))
137
            resp = self.set_headers(resp)
138
        else:
139
            sign_method, req = saml_message.gen_logout_request(response_id, session_index, user_id, IDP_IDENTITY, \
140
                    sp_meta['entityID'], service_url, CERTFILE, sign = False)
141
            # réponse utilisant le binding REDIRECT
142
            req = encode_request(req, True)
143
            req_args = sign_request(CERTFILE, sign_method, 'SAMLRequest', req, from_url)
144
            if req_args != []:
145
                redirect_url = '%s?%s' % (service_url, urllib.urlencode(req_args))
146
            resp = RedirectResponse(redirect_url)
147

    
148
        # on stocke l'id et le ticket avant de rediriger
149
        ticket.valid = False
150
        self.manager.pending_logout[sso_session][3][response_id] = ticket.ticket
151
        log.msg("%s -- %s" % (sso_session, _("Sending SAML logout request to {0} ({1}) : {2}").format(saml_identity, service_url, response_id)))
152
        return resp
153

    
154
    @trace
155
    def openid_logout_request(self, ticket, sso_session, from_url, css):
156
        """Envoi d'une requête de déconnexion à une entité partenaire et stocke les informations sur l'origine de la déconnexion"""
157
        # calcul de la requête depuis les infos du ticket
158
        state, redirect_url, needs_confirm = openid_logout_url(ticket.provider_data, self.manager, sso_session, from_url)
159
        # on stocke l'id et le ticket avant de rediriger
160
        ticket.valid = False
161
        self.manager.pending_logout[sso_session][3][state] = ticket.ticket
162
        if needs_confirm:
163
            return self.confirm_logout(redirect_url, ticket, css)
164
        else:
165
            return RedirectResponse(redirect_url)
166

    
167
    @trace
168
    def confirm_logout(self, redirect_url, ticket, css):
169
        """ Asks if user wants to disconnect from Provider
170
        """
171
        prov_label = self.manager.external_providers[ticket.provider_data['provider']][2]
172
        content = ""
173
        logout_msg = _('Do you want to disconnect from {0} ?').format(prov_label)
174
        params = {'logout_url':redirect_url + "?state=%s" % ticket.provider_data['state'],
175
                  # fallback url redirects on /logout with state to validate
176
                  # that provider was taken into account (even if not disconnected)
177
                  'fallback_url':AUTH_FORM_URL + '/logout?state=%s' % ticket.provider_data['state'],
178
                  'message':logout_msg
179
                 }
180
        header_script = """<script type="text/javascript">\n"""
181
        header_script += open('interface/scripts/confirm_logout.js').read() % params
182
        header_script += """\n</script>"""
183
        onload_script = """confirm_logout();"""
184
        resp = http.Response(stream = gen_page(_('Logout request to service'), content,
185
                                               javascript = onload_script,
186
                                               header_script=header_script,
187
                                               css=css))
188
        resp = self.set_headers(resp)
189
        return resp
190

    
191
    @trace
192
    def send_cas_logout_request(self, ticket, sso_session, use_SOAP = False):
193
        """
194
            envoie une demande de déconnexion à un client CAS par un requête SAML
195
            use_SOAP : utiliser une enveloppe SOAP pour transmettre le message si True
196
                       (versions futures du client CAS ?)
197
        """
198
        logoutRequest = saml_message.gen_cas_logout_request(ticket.ticket, use_SOAP)
199
        log.msg("%s -- %s" % (sso_session, _("Sending CAS logout request to {0}").format(ticket.service_url)))
200
        # gestion du passage ou non par un proxy http si défini pour cette application
201
        # envoi de la requête
202
        req = httprequest.HTTPRequest()
203
        self.manager.set_urllib_proxy(ticket)
204
        try:
205
            req.request(ticket.service_url, {'logoutRequest':logoutRequest})
206
        except httprequest.RequestError, e:
207
            log.msg("%s -- %s" % (ticket.session_id, _('Error sending logout request to {0} : ')\
208
                    .format(ticket.service_url)))
209
            log.msg(str(e))
210
        # retour en mode sans proxy
211
        self.manager.set_urllib_proxy()
212

    
213
    @trace
214
    def saml_logout_response(self, request, sso_session):
215
        """traite une réponse de déconnexion et met à jour les tickets correspondants dans le gestionnaire de sessions
216
        """
217
        signature_ok, saml_resp = extract_message(request, 'SAMLResponse')
218
        if not signature_ok:
219
            raise InternalError("%s : %s" % (_('signature error'), str(saml_resp)))
220
        response = samlp.LogoutResponseFromString(saml_resp)
221
        # on commence par vérifier qu'on ne rejoue pas un message déjà traité
222
        msg_id = response.id
223
        if self.manager.replayed_saml_msg(msg_id):
224
            # page d'erreur
225
            raise InternalError("%s -- %s" % (sso_session, _('response message has been replayed ({0})').format(msg_id)))
226
        resp_issuer = response.issuer.text.encode(encoding)
227
        in_response_to = response.in_response_to
228
        # Vérification de l'existence de la requête spécifiée dans in_response_to
229
        if in_response_to not in self.manager.pending_logout[sso_session][3]:
230
            # FIXME TODO: generate a saml negative saml response
231
            raise InternalError("%s : %s" % (_('corresponding request not found'), in_response_to))
232
        orig_message_data = self.manager.get_saml_msg(in_response_to)
233
        if orig_message_data.get('response_id', None) is not None:
234
            raise InternalError((_('response already received for request {0}').format(in_response_to)))
235
        #issue_instant = date_from_string(response.issue_instant)
236
        status_code = response.status.status_code.value
237
        #if response.status.status_message is not None:
238
        #    status_msg = response.status.status_message.text
239
        ticket_id = self.manager.pending_logout[sso_session][3][in_response_to]
240
        if status_code == samlp.STATUS_SUCCESS:
241
            self.manager.pending_logout[sso_session][1].append(ticket_id)
242
        # on indique que le message d'origine a  reçu une réponse
243
        self.manager.update_saml_msg(in_response_to, {'response_id':msg_id})
244
        log.msg("%s -- %s" % (sso_session, _('SAMLResponse ({0}) received from {1} in response to {2}').format('LogoutResponse', resp_issuer, in_response_to)))
245

    
246
    @trace
247
    def openid_logout_response(self, request, sso_session):
248
        """traite une réponse de déconnexion et met à jour les tickets correspondants dans le gestionnaire de sessions
249
        """
250
        in_response_to = request.args['state'][0]
251
        # Vérification de l'existence de la requête spécifiée dans in_response_to
252
        if in_response_to not in self.manager.pending_logout[sso_session][3]:
253
            # FIXME TODO: generate a saml negative saml response
254
            raise InternalError("%s : %s" % (_('corresponding request not found'), in_response_to))
255
        # le choix de la fermeture de session est à la discrétion de l'utilisateur,
256
        # dans tous les cas, on considère la demande traitée
257
        ticket_id = self.manager.pending_logout[sso_session][3][in_response_to]
258
        self.manager.pending_logout[sso_session][1].append(ticket_id)
259
        log.msg("%s -- %s" % (sso_session, _('OpenID logout performed in response to {0}').format(in_response_to)))
260

    
261
    @trace
262
    def build_logout_error(self, err_infos, sso_session, relay_state, css):
263
        """envoie une réponse de déconnexion négative en réponse à une requête
264
        """
265
        # récupération des infos nécessaires
266
        req_id, req_issuer, err_msg = err_infos
267
        issue_instant = time.time()
268
        # génération du message d'échec d'authentification
269
        sp_meta = self.manager.get_metadata(req_issuer)
270
        # on ne sait pas si le destinataire est un idp ou un sp, on cherche le service de déconnexion
271
        entity_role = 'SPSSODescriptor'
272
        if 'SingleLogoutService' not in sp_meta[entity_role]:
273
            entity_role = 'IDPSSODescriptor'
274
        try:
275
            binding, service_url, response_url = get_endpoint(sp_meta, 'SingleLogoutService', entity_role)
276
        except InternalError:
277
            traceback.print_exc()
278
            # on ne sait pas où la réponse doit être envoyée
279
            err_msg = _('no usable endpoint for {0}').format(req_issuer)
280
            return http.Response(stream = gen_page(_('Logout Error'), err_msg, css))
281
        # Envoi de la réponse à l'émetteur de la requête
282
        response_id = self.manager.gen_saml_id({'type':'LogoutResponse'})
283
        sign_method, response = saml_message.gen_logout_response(req_id,
284
                                                                 response_id,
285
                                                                 IDP_IDENTITY,
286
                                                                 response_url,
287
                                                                 CERTFILE,
288
                                                                 samlp.STATUS_UNKNOWN_PRINCIPAL,
289
                                                                 status_msg = err_msg)
290
        return self.send_logout_response(response, sign_method, sso_session, (binding, response_url, relay_state), css)
291

    
292
    @trace
293
    def process_logout_request(self, request, sso_session):
294
        """traite une demande de déconnexion et initie le processus de déconnexion
295
        """
296
        signature_ok, saml_req = extract_message(request, 'SAMLRequest')
297
        if not signature_ok:
298
            # FIXME TODO: generate a saml negative saml response
299
            raise InternalError("signature error : %s" % str(saml_req))
300
        saml_req = samlp.LogoutRequestFromString(saml_req)
301
        req_issuer = saml_req.issuer.text.encode(encoding)
302
        req_id = saml_req.id
303
        # name_id de l'utilisateur
304
        req_name_id = saml_req.name_id or saml_req.base_id or saml_req.encrypted_id
305
        req_name_id = req_name_id.text.encode(encoding)
306
        try:
307
            if not sso_session:
308
                # pas de session valide !
309
                raise InternalError(_('Invalid session'))
310
            # détection de la session en cas d'index de session fourni dans la requête
311
            app_ticket = None
312
            if saml_req.session_index:
313
                app_ticket = saml_req.session_index.text.encode(encoding)
314
            else:
315
                # on recherche un ticket correspondant à l'émetteur et au nameid fourni
316
                for ticket in self.manager.user_app_tickets.get(sso_session, []):
317
                    if ticket.saml_ident == req_issuer and ticket.nameid == req_name_id:
318
                        # on a trouvé un ticket qui correspond à l'utilisateur indiqué dans la requête
319
                        app_ticket = ticket.ticket
320
                        break
321
            if app_ticket in self.manager.saml_sessions:
322
                # session définie par l'idp distant -> on récupère l'app_ticket local correspondant
323
                app_ticket = self.manager.saml_sessions[app_ticket]
324
            if self.manager._DBAppSessionFromTicket(app_ticket):
325
                ticket = self.manager.app_sessions[app_ticket]
326
                if saml_req.session_index:
327
                    if ticket.session_id == sso_session:
328
                        if req_issuer != ticket.saml_ident:
329
                            raise InternalError(_('logout requester ({0}) does not own session').format(req_issuer))
330
                    # vérification du nameid
331
                    if (ticket.name_id != req_name_id):
332
                        raise InternalError(_('logout request for invalid session'))
333
                    # début d'un logout initié par un participant de la session
334
                    self.manager.pending_logout[sso_session] = [samlp.STATUS_SUCCESS, [app_ticket], (app_ticket, req_id, req_issuer), {}, [], None]
335
                    # on définit le ticket comme non valide
336
                    ticket.valid = False
337
                else:
338
                    raise InternalError(_('logout request for invalid session'))
339
            else:
340
                raise InternalError(_('Invalid session'))
341
            log.msg("%s -- %s" % (sso_session, _('SAMLRequest ({0}) received from {1}').format('LogoutRequest', req_issuer)))
342
        except InternalError, msg:
343
            # On stocke les informations nécessaires à la génération du message d'erreur
344
            log.msg("%s -- %s" % (sso_session, _('Invalid SAMLRequest ({0}) received from {1}').format('LogoutRequest', req_issuer)))
345
            return False, (req_id, req_issuer, str(msg))
346
        return True , sso_session
347

    
348
    @trace
349
    def check_attached_sessions(self, sso_session, closed_services, failed_services, from_url, css):
350
        """Vérifie les tickets rattachés à la session en cours et envoie les demandes de déconnexion nécessaires
351
        maintient la liste échecs/succés de chaque demande
352
        """
353
        # recherche des sessions non traitées lors du logout
354
        self.manager.pending_logout[sso_session][0] = samlp.STATUS_SUCCESS
355
        services_done = self.manager.pending_logout[sso_session][4]
356
        if from_url not in services_done:
357
            services_done.append(from_url)
358
        logout_call_list = []
359
        # on regarde si le logout a été initié par requête d'une entité partenaire
360
        if self.manager.pending_logout[sso_session][2]:
361
            req_ticket, req_id, req_issuer = self.manager.pending_logout[sso_session][2]
362
        else:
363
            req_ticket = req_id = req_issuer = None
364
        for ticket in self.manager.user_app_tickets[sso_session]:
365
            if hasattr(ticket,'saml_ident'):
366
                if ticket.ticket != req_ticket:
367
                    if ticket.valid:
368
                        if hasattr(ticket, 'provider_data'):
369
                            # ticket correspondant à une session initiée par un provider OpenID connu
370
                            client = self.manager.external_providers[ticket.provider_data['provider']][0]
371
                            if not client.can_logout:
372
                                # si la déconnexion n'est pas gérée, on passe au ticket suivant
373
                                self.manager.pending_logout[sso_session][1].append(ticket.ticket)
374
                                ticket.valid = False
375
                                continue
376
                            else:
377
                                # sinon demande de déconnexion auprès du fournisseur
378
                                return self.openid_logout_request(ticket, sso_session, from_url, css)
379
                        else:
380
                            # ticket correspondant à une session 'saml' valide
381
                            # -> Envoi d'une demande de déconnexion auprès de l'entité partenaire
382
                            return self.saml_logout_request(ticket, sso_session, from_url, css)
383
                    else:
384
                        # Une déconnexion a déjà été demandée pour ce ticket
385
                        # -> On met à jour le status du logout en cours
386
                        if ticket.saml_ident in self.manager.external_providers:
387
                            service_name = self.manager.external_providers[ticket.saml_ident][2]
388
                        else:
389
                            service_name = ticket.saml_ident
390
                        if ticket.ticket in self.manager.pending_logout[sso_session][1]:
391
                            closed_services.append(service_name)
392
                        else:
393
                            failed_services.append(service_name)
394
                            # Au moins une des demandes envoyées n'a pas reçu de réponse favorable (logout partiel)
395
                            self.manager.pending_logout[sso_session][0] = samlp.STATUS_PARTIAL_LOGOUT
396
            else:
397
                if SEND_CAS_LOGOUT:
398
                    # ticket CAS standard, on envoie une requête de logout
399
                    # au client CAS (sur l'url de service du ticket).
400
                    if ticket.service_url not in services_done:
401
                        services_done.append(ticket.service_url)
402
                        if not ticket.ticket.startswith('PT-'):
403
                            # on n'envoie pas de demande de déconnexion
404
                            # sur les services authentifiés en mode proxy (imap, ftp, ...)
405
                            logout_call = (self.send_cas_logout_request,
406
                                        [ticket, sso_session, False],
407
                                        {})
408
                            logout_call_list.append(logout_call)
409
        if logout_call_list:
410
            # On appelle l'ensemble des logouts cas dans un thread
411
            callMultipleInThread(logout_call_list)
412
        return None
413

    
414

    
415
    @trace
416
    def build_logout_response(self, sso_session, relay_state, css):
417
        """construit une réponse à destination de l'entité ayant demandé la déconnexion
418
        """
419
        # Lecture des informations sur l'initiateur de la déconnexion
420
        app_ticket, req_id, req_issuer  = self.manager.pending_logout[sso_session][2]
421
        # recherche du role du partenaire (fournisseur de service/d'identité)
422
        saml_role = self.manager.app_sessions[app_ticket].saml_role
423
        # Si le logout est effectué suite à une LogoutRequest, on envoie une réponse au demandeur
424
        sp_meta = self.manager.get_metadata(req_issuer)
425
        try:
426
            binding, service_url, response_url = get_endpoint(sp_meta, 'SingleLogoutService', saml_role)
427
        except InternalError:
428
            traceback.print_exc()
429
            err_msg = _('no usable endpoint for {0}').format(req_issuer)
430
            return http.Response(stream = gen_page(_('Logout Error'), err_msg, css))
431
        # Envoi de la réponse finale à l'émetteur de la requête
432
        response_id = self.manager.gen_saml_id({'type':'LogoutResponse'})
433
        sign_method, response = saml_message.gen_logout_response(req_id, response_id, IDP_IDENTITY, response_url, CERTFILE, \
434
                self.manager.pending_logout[sso_session][0])
435
        return self.send_logout_response(response, sign_method, sso_session, (binding, response_url, relay_state), css)
436

    
437
    def send_logout_response(self, response, sign_method, sso_session, dest_info, css):
438
        binding, response_url, relay_state = dest_info
439
        if binding == samlp.BINDING_HTTP_REDIRECT:
440
            # Réponse avec le binding REDIRECT
441
            response = encode_request(response, True)
442
            # la signature doit se faire sur la chaine suivante : SAMLResponse=value&RelayState=value&SigAlg=value -> encodé dans location header
443
            req_args = sign_request(CERTFILE, sign_method, 'SAMLResponse', response, relay_state)
444
            if req_args != []:
445
                redirect_url = '%s?%s' % (response_url, urllib.urlencode(req_args))
446
            else:
447
                redirect_url = response_url
448
            resp = RedirectResponse(redirect_url)
449
        else:
450
            # Réponse avec le binding POST (default fallback)
451
            response = encode_request(response)
452
            relay_info = ""
453
            if relay_state:
454
                relay_info = """<input type="hidden" name="RelayState" value="%s">""" % relay_state
455
            form = """
456
                    <FORM action="%s" method="POST">
457
                    <input type="hidden" name="SAMLResponse" value="%s">
458
                    %s
459
                    <input type="hidden" name="SigAlg" value="%s">
460
                    <p class="formvalidation">
461
                    <input class="btn" type="submit" value="%s"></p>
462
            """ % (response_url, split_form_arg(response), relay_info, sign_method, _('Logout confirmation to service'))
463
            resp = http.Response(stream = gen_page(_('Logout confirmation to service'), form, css))
464
            resp = self.set_headers(resp)
465
        log.msg("%s -- %s" % (sso_session, _("SAMLResponse ({0}) sent to {1}").format('LogoutResponse', response_url)))
466
        return resp
467

    
468
    @trace
469
    def build_logout_page(self, sso_session, closed_services, failed_services, from_url, css):
470
        """construit la page à afficher en cas de déconnexion initiée localement
471
        """
472
        logout_msg = ""
473
        default_url, force_default = self.manager.get_default_logout_url(sso_session)
474
        if not from_url or force_default:
475
            from_url = default_url
476
        if closed_services != []:
477
            logout_msg += """<b><font color="green">%s</font></b>""" % _("Following service providers have been disconnected")
478
            logout_msg += "<br/><table border=0><tr><td>%s</td></tr></table>" % "</td></tr><tr><td>".join(closed_services)
479
        if failed_services != []:
480
            logout_msg += """<b><font color="orange">%s</font></b>""" % _("Following service providers did not report successful logout")
481
            logout_msg += "<br/><table border=0><tr><td>%s</td></tr></table>" % "</td></tr><tr><td>".join(failed_services)
482
        if from_url:
483
            logout_msg += """
484
            <br/><b>%s</b> : <br/><h2><font color="blue">%s</font></h2><br/>
485
            </p><p class="formvalidation">
486
            <button type="button" class="btn" onclick="javascript:location.href='%s'">%s</button></p>
487
            """ % (_('Following page has been proposed'), from_url, from_url, _('Proceed to above url'))
488
        else:
489
            logout_msg += """<br/><form action="/login" method="POST">
490
            <p class="formvalidation">
491
            <input class="btn" type="submit" value="%s"></p></form>""" % _('New session')
492
        if self.manager.pending_logout[sso_session][0] == samlp.STATUS_PARTIAL_LOGOUT:
493
            logout_status = _("Partial single logout success")
494
        else:
495
            logout_status = _("Single logout success")
496
        resp = http.Response(stream = gen_page(logout_status, str(logout_msg), css))
497
        return self.set_headers(resp)
498

    
499
    def _render(self, request):
500
        """Gestion de la déconnexion des sessionss SSO
501
        """
502
        # vérification de la session en cours
503
        cookie = None
504
        authenticated = False
505
        cookies = getCookies(request)
506
        sso_session = None
507
        for cookie in cookies:
508
            if cookie.name == 'EoleSSOServer':
509
                sso_session = cookie.value
510
                authenticated = self.manager.validate_session(sso_session)
511
                break
512

    
513
        css = DEFAULT_CSS
514
        # vérifie si on doit rediriger après la déconnexion
515
        from_url = None
516
        relay_state = None
517
        if 'RelayState' in request.args:
518
            relay_state = request.args['RelayState'][0]
519
        if request.args.has_key('url'):
520
            from_url = request.args['url'][0]
521
        elif request.args.has_key('service'):
522
            from_url = request.args['service'][0]
523
        elif relay_state:
524
            # XXX FIXME : vérifier que relay_state est une url ?
525
            from_url = relay_state
526
        # recherche d'une eventuelle css associée
527
        if from_url:
528
            service_filter = self.manager._check_filter(from_url)[0]
529
            if service_filter not in ['default', '']:
530
                if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
531
                    css = service_filter
532
        try:
533
            css = request.args['css'][0]
534
        except KeyError:
535
            pass
536
        closed_services = []
537
        failed_services = []
538
        resp = None
539
        saml_resp = False
540

    
541
        try:
542
            ########################
543
            # Gestion du logout SAML
544
            if not authenticated:
545
                # pas d'authentification détectée
546
                sso_session = None
547
                if not 'SAMLRequest' in request.args:
548
                    # logout initié localement, on devrait être authentifié
549
                    raise InternalError(_('Invalid session'))
550
            # traitement d'une requête reçue : LogoutRequest ou LogoutResponse
551
            if 'SAMLResponse' in request.args and sso_session in self.manager.pending_logout:
552
                # test sur pending logout : hack car dans certains cas firefox semble rejouer une requête de logout
553
                # vers le sp sans que l'idp l'aie envoyée
554
                self.saml_logout_response(request, sso_session)
555
            if 'state' in request.args and sso_session in self.manager.pending_logout:
556
                self.openid_logout_response(request, sso_session)
557
            if 'SAMLRequest' in request.args:
558
                req_ok, err_infos = self.process_logout_request(request, sso_session)
559
                if not req_ok:
560
                    # requête non valide, on renvoie un échec de déconnexion
561
                    return self.build_logout_error(err_infos, sso_session, relay_state, css)
562
            elif sso_session not in self.manager.pending_logout:
563
                # Logout initié localement (idp initiated), on conserve une trace de relay_state et from_url
564
                # au cas où on aurait des redirections à faire (requêtes SAML)
565
                self.manager.pending_logout[sso_session] = [samlp.STATUS_SUCCESS, [], None, {}, [], (from_url, relay_state)]
566
            # envoi de requêtes de déconnexion pour chaque session attachée à la session SSO
567
            resp = self.check_attached_sessions(sso_session, closed_services, failed_services, from_url, css)
568
            if resp is not None:
569
                return resp
570
            # Tous les participants à la session sont prévenus, calcul du résultat final
571
            # on regarde si une url de redirection était présente dans la requête d'origine
572
            if self.manager.pending_logout[sso_session][5] is not None:
573
                from_url, relay_state = self.manager.pending_logout[sso_session][5]
574
            if self.manager.pending_logout[sso_session][2]:
575
                # Envoi d'une réponse à la demande de déconnexion
576
                saml_resp = True
577
                resp = self.build_logout_response(sso_session, relay_state, css)
578
            else:
579
                # affichage d'un message (déconnexion locale)
580
                resp = self.build_logout_page(sso_session, closed_services, failed_services, from_url, css)
581

    
582
            ############################
583
            # Logout SSO Session Globale
584

    
585
            default_url, force_default = self.manager.get_default_logout_url(sso_session)
586
            # si la session vient d'une fédération avec un idp distant,
587
            # on regarde si une url de retour est configurée (et obligatoire)
588
            if force_default:
589
                from_url = default_url
590
            # suppression finale de la session sso et des données associées
591
            self.manager.logout(sso_session)
592
        except InternalError, err:
593
            log.msg(err)
594
        if not saml_resp:
595
            # logout non initié par une requête saml, on affiche la page de compte rendu de déconnexion
596
            # ou on redirige vers l'url spécifiée par 'url' ou 'service'
597
            if from_url and failed_services == []:
598
                # Dans le cas ou on doit rediriger et aucune erreur n'est survenue -> pas d'affichage
599
                # XXX FIXME : afficher un formulaire d'avertissement dans tous les cas ?
600
                resp = RedirectResponse(from_url)
601
                log.msg(_("Logged out : redirecting to {0}").format(from_url))
602
            elif resp is None:
603
                resp = RedirectResponse('/')
604

    
605
        # expiration du cookie EoleSSO
606
        if sso_session and cookie:
607
            cookie.expires = 1
608
            resp.headers.setHeader('Set-Cookie', cookies)
609
        return resp
610

    
611
class SamlMetadata(SamlResource):
612

    
613
    def __init__(self, manager):
614
        self.manager = manager
615
        super(SamlMetadata, self).__init__()
616

    
617
    def _render(self, request):
618
        """Affiche les métadonnées SAML du serveur local"""
619
        data = gen_metadata(self.manager, AUTH_FORM_URL, CERTFILE)
620
        resp = http.Response(stream = data.encode(encoding))
621
        return self.set_headers(resp, {'Content-type': ('application/xml',)})
622

    
623
class SamlErrorPage(SamlResource):
624
    """
625
    page d'erreur http
626
    """
627
    addSlash = False
628

    
629
    def __init__(self, code, description):
630
        self.code = code
631
        self.description = description
632
        title = ('EoleSSO : Erreur')
633
        super(SamlErrorPage, self).__init__()
634

    
635
    def _render(self, request):
636
        return http.StatusResponse(code = self.code, description = self.description)
637

    
638
class SamlConsumer(SamlResource):
639
    """Resource de traitement des demandes d'accès aux service tiers
640
    (Fournisseur de service)
641
    """
642
    addSlash = False
643

    
644
    def __init__(self, manager):
645
        self.manager = manager
646
        super(SamlConsumer, self).__init__()
647

    
648
    def _render(self, request):
649
        """Assertion Consuming processing
650
        """
651
        # vérification d'une éventuelle session en cours
652
        authenticated = False
653
        cookies = getCookies(request)
654
        sso_session = None
655
        for cookie in cookies:
656
            if cookie.name == 'EoleSSOServer':
657
                sso_session = cookie.value
658
                # On vérifie si la session est valide
659
                authenticated = self.manager.validate_session(sso_session)
660
                break
661
        # XXX FIXME : si déjà authentifié: supprimer le cookie actuel ou vérifier qu'il correspond au bon utilisateur ?
662
        # pb: single logout de l'ancienne session ?
663
        css = DEFAULT_CSS
664
        # vérification de l'adresse où l'utilisateur doit être redirigé
665
        return_url = None
666
        relay_state = None
667
        if 'RelayState' in request.args:
668
            relay_state = request.args['RelayState'][0]
669
        # recherche d'une eventuelle css associée
670
        if relay_state:
671
            # on regarde si RelayState provient d'une requête émise précédemment
672
            return_url = self.manager.get_relay_data(relay_state)
673
            if return_url is None:
674
                # relay_state fourni par le fournisseur d'identité
675
                return_url = relay_state
676
            service_filter = self.manager._check_filter(return_url)[0]
677
            if service_filter not in ['default', '']:
678
                if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
679
                    css = service_filter
680
        try:
681
            css = request.args['css'][0]
682
        except KeyError:
683
            pass
684
        try:
685
            ###########################
686
            # traitement de l'assertion
687
            if authenticated:
688
                # l'utilisateur est déjà authentifié
689
                pass
690
                # XXX FIXME vérifier que l'utilisateur de l'assertion correspond ?
691
            if 'SAMLResponse' not in request.args:
692
                # pas d'authentification détectée
693
                raise InternalError(_('SAMLResponse expected'))
694
            else:
695
                # Traitement de la requête reçue (AuthnStatement)
696
                success, infos =  process_assertion(self.manager, request)
697

    
698
        except InternalError, e:
699
            traceback.print_exc()
700
            return self.set_headers(gen_page_err(str(e), return_url, css))
701
        # l'assertion reçue est valide, on la traite
702
        if not success:
703
            content = _('Federation : No local user does match specified attributes')
704
            return self.set_headers(gen_page_err(content, return_url, css))
705
        else:
706
            for assertion_id, data in infos.items():
707
                # si des attributs sont remontés et valides, on cherche l'utilisateur correspondant (fédération par attribut commun)
708
                session_index, assertion_issuer, resp_attrs, name_id, auth_instant, class_ref = data
709
                # si pas de destination spécifiée (dans RelayState ou dans la requête d'origine),
710
                # on regarde si une destination par défaut existe pour ce fournisseur d'identité
711
                if not return_url:
712
                    idp_opts = self.manager.get_federation_options(assertion_issuer)
713
                    if 'default_service' in idp_opts:
714
                        return_url = idp_opts['default_service']
715
                content = u"<h2>%s</h2>" % (_("you have been authenticated by identity provider {0}").format(assertion_issuer))
716
                # On essaye de créer une session pour l'utilisateur local correspondant (ou on reprend l'existante)
717
                if not authenticated:
718
                    sso_session = None
719
                # on regarde si la branche de recherche de l'utilisateur peut être déduite des attributs
720
                # (ex : n°rne présent)
721
                defer_federation = self.manager.authenticate_federated_user(resp_attrs, assertion_issuer, auth_instant, class_ref, sso_session)
722
                return defer_federation.addBoth(self.callb_federation, return_url, assertion_id, data, content, css, request, cookies)
723
            return self.set_headers(gen_page_err(_("NO USER FOUND"), return_url, css))
724

    
725
    @trace
726
    def callb_federation(self, result_fed, return_url, assertion_id, data, content, css, request, cookies):
727
        session_id, user_data = result_fed
728
        session_index, assertion_issuer, resp_attrs, name_id, auth_instant, class_ref = data
729
        if session_id and not isinstance(session_id, failure.Failure):
730
            # Mise en place de cookie SSO pour la session locale
731
            cookies = []
732
            for cookie in getCookies(request):
733
                if cookie.name != 'EoleSSOServer':
734
                    cookies.append(cookie)
735
            cookies.append(TwCookie("EoleSSOServer", session_id, path = "/", discard=True, secure=True))
736
            # Si une session existe pour ce fournisseur de service, on le supprime
737
            self.manager.remove_old_ticket(session_id, assertion_issuer)
738
            # On stocke les informations de session dans un ticket fictif (pour permettre le logout)
739
            sp_meta = self.manager.get_metadata(assertion_issuer)
740
            # recherche d'un éventuel index pour le choix du binding
741
            endpoint_index = None
742
            if 'index' in request.args:
743
                endpoint_index = request.args['index'][0]
744
            try:
745
                binding, service_url, response_url = get_endpoint(sp_meta, 'SingleSignOnService',
746
                                                                  ent_type = 'IDPSSODescriptor',
747
                                                                  index=endpoint_index)
748
            except InternalError:
749
                # XXX FIXME
750
                err_msg = _('no usable endpoint for {0}').format(assertion_issuer)
751
                log.msg(err_msg)
752
                return self.set_headers(http.Response(stream = gen_page(_('Assertion consuming error'), err_msg, css)))
753
            app_ticket = self.manager.get_app_ticket(session_id, response_url, idp_ident=assertion_issuer)
754
            if app_ticket:
755
                # pour les applications saml, on utilise le filtre saml par défaut (pour avoir au moins l'attribut de federation)
756
                if self.manager.app_sessions[app_ticket].filter == 'default':
757
                    self.manager.app_sessions[app_ticket].filter = 'saml'
758
                # application de la css correspondante si existante
759
                service_filter = self.manager.app_sessions[app_ticket].filter
760
                if service_filter not in ['default', '']:
761
                    if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
762
                        css = service_filter
763
                # information supplémentaires à conserver
764
                self.manager.app_sessions[app_ticket].saml_ident = assertion_issuer
765
                self.manager.app_sessions[app_ticket].saml_role = 'IDPSSODescriptor'
766
                self.manager.app_sessions[app_ticket].response_id = assertion_id
767
                self.manager.app_sessions[app_ticket].name_id = name_id
768
                self.manager.app_sessions[app_ticket].idp_session_index = session_index
769
                if 'uaj' in sp_meta:
770
                    # ajout de l'uaj établissement si disponible dans les métadonnées (spécifique Education/Eole)
771
                    self.manager.app_sessions[app_ticket].uaj = sp_meta.get('uaj')
772
                if self.manager.app_sessions[app_ticket].timeout_callb.cancelled == 0:
773
                    # ce ticket expirera seulement à la fermeture de la session SSO
774
                    self.manager.app_sessions[app_ticket].timeout_callb.cancel()
775
                self.manager.saml_sessions[session_index] = app_ticket
776
            # XXX FIXME : Debug
777
            if return_url is None:
778
                if DEBUG_LOG:
779
                    content += """assertion_id = %s<br/><hr/>
780
        idp session_index = %s<br/>
781
        attributes = %s<br/>
782
        authentication context = %s<br/><hr/>
783
        local session created<br/>""" % (assertion_id, session_index, str(resp_attrs), class_ref)
784
                else:
785
                    content += "<br>%s" % _("no destination service given")
786
            # content += """%s: %s<br/>""" % (_('Redirecting to '), return_url)
787
        else:
788
            if isinstance(session_id, failure.Failure):
789
                log.msg('Failure searching federated_user : %s' % session_id.getErrorMessage())
790
            content = _('Federation : No local user does match specified attributes')
791
            resp = gen_page_err(content, return_url, css)
792
            return self.set_headers(gen_page_err)
793
        return self.redirect_after_federation(return_url, content, css, cookies)
794

    
795
    @trace
796
    def redirect_after_federation(self, return_url, content, css, cookies):
797
        if return_url:
798
            resp = RedirectResponse(return_url)
799
        else:
800
            resp = http.Response(stream = gen_page('Request processed', content, css))
801
            resp = self.set_headers(resp)
802
        resp.headers.setHeader('Set-Cookie', cookies)
803
        return resp
804

    
805
class SamlResponse(SamlResource):
806
    """ressource principale
807
    traitement de l'envoi d'assertions SAML
808
    (Fournisseur d'identité)
809
    """
810

    
811
    def __init__(self, manager):
812
        self.manager = manager
813
        super(SamlResponse, self).__init__()
814

    
815
    def _render(self, request):
816
        """Génère une réponse SAML2 correspondant au profil HTTP-POST
817

818
        - verifie qu'une session SSO est déjà en cours
819
        - sinon, essaie d'en établir une
820
        - construit une Reponse SAML (contenant une assertion ou non) et l'envoie au fournisseur de service (entityID)
821
        """
822
        app_ticket = None
823
        relay_state = None
824
        app_ticket = None
825
        req_id = None
826
        sp_ident = None
827
        passive = False
828
        force_auth = False
829
        requested_contexts = []
830
        comparison = ""
831
        login_ticket = None
832
        css = DEFAULT_CSS
833
        compressed = True
834
        if 'ticket' in request.args:
835
            #compressed = False
836
            app_ticket = request.args['ticket'][0]
837
        # recherche d'un éventuel index pour le choix du binding
838
        endpoint_index = None
839
        if 'index' in request.args:
840
            endpoint_index = request.args['index'][0]
841
        if 'SAMLRequest' in request.args:
842
            # requête SAML AuthnRequest reçue
843
            request_data = process_auth_request(self.manager, request, compressed=compressed)
844
            # stockage des données de la requête pour gérer la réponse (on génère un ticket 'LT' avec les données associées)
845
            sp_ident, response_url, binding, req_id, passive, force_auth, comparison, requested_contexts = request_data
846
            login_ticket = self.manager.get_login_ticket(req_id)
847
            # stockage temporaire du résultat pour conserver les informations d'origine lorsqu'une redirection
848
            # sur la page d'authentification est nécessaire
849
            req_ticket = self.manager.gen_relay_state(request_data)
850
            log.msg("%s : %s" % (_('SAMLRequest ({0}) received from {1}').format('AuthnRequest', sp_ident), req_id))
851
            sp_meta = self.manager.get_metadata(sp_ident)
852
            # on vérifie que l'on est capable de gérer le niveau de sécurité demandé (requested_context)
853
            if requested_contexts:
854
                ctx_ok, required_ctx = check_required_contexts(comparison, requested_contexts)
855
                if not ctx_ok:
856
                    # XXX FIXME TODO: generate a saml negative saml response or display local error page (invalid context) ?
857
                    resp = http.Response(stream = gen_page(_('Unreachable resource'), required_ctx, css))
858
                    return self.set_headers(resp)
859
        elif 'ReqInfos' in request.args:
860
            # reprise des informations de la requête après nouvelle authentification
861
            req_ticket = request.args['ReqInfos'][0]
862
            try:
863
                request_data = self.manager.get_relay_data(req_ticket)
864
                sp_ident, response_url, binding, req_id, passive, force_auth, comparison, requested_contexts = request_data
865
                sp_meta = self.manager.get_metadata(sp_ident)
866
            except:
867
                # XXX FIXME TODO: generate a negative saml response or display local error page (invalid context) ?
868
                resp = http.Response(stream = gen_page(_('Unreachable resource'), 'ReqInfos=%s' % req_ticket, css))
869
                return self.set_headers(resp)
870
        else:
871
            # Authentification initiée par l'IDP
872
            if 'sp_ident' in request.args:
873
                sp_ident = request.args['sp_ident'][0]
874
                # recherche des metadonnées du fournisseur de services
875
                sp_meta = self.manager.get_metadata(sp_ident)
876
                sp_ident = sp_meta.get('entityID', sp_ident)
877
                # recherche de l'adresse à laquelle envoyer les assertions
878
                try:
879
                    binding, service_url, response_url = get_endpoint(sp_meta, 'AssertionConsumerService', index=endpoint_index)
880
                except InternalError:
881
                    traceback.print_exc()
882
                    err_msg = _('no usable endpoint for {0}').format(sp_ident)
883
                    resp = http.Response(stream = gen_page(_('Unreachable resource'), err_msg, css))
884
                    return self.set_headers(resp)
885
            else:
886
                # XXX FIXME TODO: generate a negative saml response or display local error page (invalid issuer) ?
887
                err_msg = _("Missing parameter")
888
                resp = http.Response(stream = gen_page(_('Unreachable resource'), err_msg, css))
889
                return self.set_headers(resp)
890

    
891
        # verification de l'authentification
892
        authenticated = False
893
        session_id = None
894
        user_id = ""
895
        user_data = {}
896
        cookies = getCookies(request)
897
        from_credentials = False
898
        for cookie in cookies:
899
            if cookie.name == 'EoleSSOServer':
900
                session_id = cookie.value
901
                # vérification de la validité de la session
902
                authenticated = self.manager.validate_session(session_id)
903
        # cas ou une (ré)authentification auprès de l'utilisateur est nécessaire
904
        if not authenticated or (force_auth and not app_ticket):
905
            if not passive:
906
                redirect_args = []
907
                for argname, argvalue in request.args.items():
908
                    if argname == "SAMLRequest":
909
                        # requête d'authentification : on conserve en cache le résultat du processing
910
                        # pour ne pas l'effectuer une 2ème fois.
911
                        argname = "ReqInfos"
912
                        argvalue = [req_ticket]
913
                    redirect_args.append((argname, argvalue[0]))
914
                req_args = [('service', "%s/saml?%s" % (AUTH_FORM_URL, urllib.urlencode(redirect_args)))]
915
                if force_auth:
916
                    req_args.append(('renew', 'true'))
917
                if login_ticket:
918
                    req_args.append(('lt', login_ticket))
919
                redirect_url = '%s?%s' % (AUTH_FORM_URL, urllib.urlencode(req_args))
920
                resp = RedirectResponse(redirect_url)
921
                return resp
922
            elif req_id:
923
                # mode passif : on continue sans authentifier (réponse négative)
924
                sign_method, response = saml_message.gen_status_response(CERTFILE, IDP_IDENTITY, response_url, samlp.STATUS_NO_PASSIVE, req_id, _("no previous authentication"))
925

    
926
        # vérification des paramètres passés dans la requête
927
        try:
928
            css = request.args['css'][0]
929
        except KeyError:
930
            css = DEFAULT_CSS
931
        if 'RelayState' in request.args:
932
            relay_state = request.args['RelayState'][0]
933
        # La session est simulée à travers un ticket CAS factice
934
        # on utilise l'url de traitement des assertions pour permettre un filtrage sur le service de destination
935
        if app_ticket:
936
            # On regarde si le ticket a été délivré au moment de l'authentification
937
            ticket = self.manager.app_sessions[app_ticket]
938
            if ticket.session_id != session_id or \
939
               ticket.from_credentials != True or \
940
               ticket.service_url != "%s/saml" % (AUTH_FORM_URL):
941
                app_ticket = None
942
        if not app_ticket and authenticated:
943
            # Si une session existe déjà pour ce fournisseur de service, on la supprime
944
            self.manager.remove_old_ticket(session_id, sp_ident)
945
            # on génère un nouveau ticket pour ce fournisseur de service
946
            app_ticket = self.manager.get_app_ticket(session_id, response_url)
947
            ticket = self.manager.app_sessions[app_ticket]
948
        if app_ticket:
949
            # modification du service du ticket pour correspondre à l'entité partenaire
950
            ticket.service_url = get_service_from_url(response_url)
951
            if sp_ident in self.manager.apps['sp_ident']:
952
                ticket.filter = self.manager.apps['sp_ident'][sp_ident][0]
953
            else:
954
                id_filter = self.manager._check_filter(ticket.service_url)[0]
955
                ticket.filter = id_filter
956
            # pour les applications saml, on utilise le filtre saml par défaut (pour avoir au moins l'attribut de federation)
957
            if self.manager.app_sessions[app_ticket].filter == 'default':
958
                self.manager.app_sessions[app_ticket].filter = 'saml'
959
            # application de la css correspondant au filtre si existante
960
            service_filter = self.manager.app_sessions[app_ticket].filter
961
            if service_filter not in ['default', ''] and css == DEFAULT_CSS:
962
                if os.path.exists(os.path.join('interface', '%s.css' % service_filter)):
963
                    css = service_filter
964
            # Génrération d'un attribut nameid aléatoire pour cette session
965
            # Le fournisseur de service peut spécifier les attributs requis dans leurs métadonnées
966
            # (AttributeConsumingService/RequestedAttribute dans le xml)
967
            # Tous les attributs définis dans le filtre d'appplication seront envoyés
968
            user_id = gen_random_id('NAMEID_')
969
            response_id = self.manager.gen_saml_id({'type':'SamlResponse'})
970
            # Stockage d'informations supplémentaires dans le ticket d'application
971
            # sp identifier
972
            # response id
973
            # id d'authnRequest si present ?
974
            self.manager.app_sessions[app_ticket].saml_ident = sp_ident
975
            self.manager.app_sessions[app_ticket].saml_role = 'SPSSODescriptor'
976
            self.manager.app_sessions[app_ticket].response_id = response_id
977
            self.manager.app_sessions[app_ticket].name_id = user_id
978
            if 'uaj' in sp_meta:
979
                # ajout de l'uaj établissement si disponible dans les métadonnées (spécifique Education/Eole)
980
                self.manager.app_sessions[app_ticket].uaj = sp_meta.get('uaj')
981
            # ticket de type SAML : expire seulement si demandé
982
            if self.manager.app_sessions[app_ticket].timeout_callb.cancelled == 0:
983
                self.manager.app_sessions[app_ticket].timeout_callb.cancel()
984
            # récupération des données disponibles pour le service à atteindre
985
            app_ok, user_data = self.manager.get_user_details(app_ticket, response_url, renew=False, sections=True, keep_valid=True)
986
            data, filter_data = user_data
987
            user_data = {}
988
            # modification du nom des attributs en fonction du filtre
989
            for attrs in filter_data.values():
990
                for name, label in attrs.items():
991
                    user_data[name] = data[label]
992
            from_credentials = self.manager.app_sessions[app_ticket].from_credentials
993

    
994
            attr_table = []
995
            for attr, val in user_data.items():
996
                if type(val) == list:
997
                    val = ', '.join(val)
998
                if val == "":
999
                    val = "&nbsp;"
1000
                attr_table.append('<tr><td>%s</td><td>%s</td></tr>' % (attr, val))
1001
            attr_msg = _('Following attributes will be sent')
1002
            # récupération de la date d'authentification
1003
            auth_instant = self.manager.get_auth_instant(app_ticket)
1004
            # on récupère le type d'authentification de la session SSO
1005
            auth_class = self.manager.get_auth_class(app_ticket)
1006
            # récupération de l'adresse du client effectuant la demande
1007
            addr_client = request.chanRequest.getRemoteHost().host
1008
            try:
1009
                dns_client = socket.gethostbyaddr(addr_client)[0]
1010
            except:
1011
                # pas de résolution dns possible
1012
                dns_client = None
1013
            sign_method, response = saml_message.gen_response(response_id, authenticated, auth_instant, \
1014
                                    app_ticket, from_credentials, user_id, user_data, req_id, response_url, \
1015
                                    IDP_IDENTITY, sp_ident, CERTFILE, auth_class, addr_client, dns_client)
1016

    
1017
        if request.args.has_key('show') and DEBUG_LOG:
1018
            resp = http.Response(stream = response.encode(encoding))
1019
            return self.set_headers(resp, {'Content-type': ('application/xml',)})
1020
        if DISPLAY_FEDERATION == True and user_data:
1021
            # affichage des attributs envoyés
1022
            msg = """<table border=1><tr><th colspan=2>%s</th></tr>%s</table>""" % (attr_msg, '\n'.join(attr_table))
1023
            submit_input = """<p class="formvalidation"><input class="btn" type="Submit" value="%s"></p>""" % _('Proceed to service')
1024
        else:
1025
            msg = _('Please wait, accessing requested service...')
1026
            submit_input = ""
1027
        if binding == samlp.BINDING_HTTP_REDIRECT:
1028
            # REDIRECT response
1029
            response = encode_request(response, True)
1030
            req_args = sign_request(CERTFILE, sign_method, 'SAMLResponse', response, relay_state)
1031
            if req_args != []:
1032
                redirect_url = '%s?%s' % (response_url, urllib.urlencode(req_args))
1033
            else:
1034
                redirect_url = response_url
1035
            resp = RedirectResponse(redirect_url)
1036
        else:
1037
            # POST response (fallback par défaut)
1038
            response = encode_request(response)
1039
            relay_info = ""
1040
            if relay_state:
1041
                relay_info = """<input type="hidden" name="RelayState" value="%s">""" % relay_state
1042

    
1043
            form = """%s<br/><div class='ressource' id="wait_msg"></div><br/>
1044
<FORM name="federate_user" action="%s" method="POST" onSubmit="return aff_wait()">
1045
<input type="hidden" name="SAMLResponse" value="%s">
1046
%s
1047
<input type="hidden" xx name="SigAlg" value="%s">
1048
%s
1049
</FORM>""" % (msg, response_url, response, relay_info, sign_method, submit_input)
1050
            # recherche du nom 'humain' de l'entité si disponible
1051
            entity_name = sp_meta.get('entity_local_id', '')
1052
            wait_msg = _("Awaiting response from ")
1053
            header_script = """<script type="text/javascript">
1054
function aff_wait()
1055
{
1056
    div_msg = document.getElementById('wait_msg');
1057
    div_msg.innerHTML = "%s<span>%s</span>";
1058
    return true;
1059
}
1060
</script>
1061
""" % (wait_msg, response_url)
1062
            head = "%s : <br/>%s" % (_('You are being Authenticated to entity'), entity_name or sp_ident)
1063
            if DISPLAY_FEDERATION == False or user_data == {}:
1064
                head = _('Accessing to service')
1065
                form += """<script type="text/javascript">aff_wait();document.federate_user.submit();</script>"""
1066
            resp = http.Response(stream = gen_page(head, form, css, header_script=header_script))
1067
            resp = self.set_headers(resp)
1068
        if session_id:
1069
            log_prefix = "%s -- " % session_id
1070
        else:
1071
            log_prefix = ""
1072
        log.msg("%s%s" % (log_prefix, _('SAMLResponse ({0}) sent to {1}').format('AuthnStatement', sp_ident)))
1073

    
1074
        return resp
1075

    
1076
    def locateChild(self, request, segments):
1077
        if segments and segments[0] == 'metadata':
1078
            return SamlMetadata(self.manager), ()
1079
        if segments and segments[0] == 'acs':
1080
            return SamlConsumer(self.manager), ()
1081
        if segments and segments[0] == 'logout':
1082
            return SamlLogout(self.manager), ()
1083
        if segments and segments[0] == 'discovery':
1084
            return IDPDiscoveryService(self.manager), ()
1085
        if segments == ():
1086
            return self, ()
1087
        return SamlErrorPage(responsecode.NOT_FOUND, "%s : %s" % (_('Unreachable resource'),  '/'.join(segments))), ()
1088

    
1089
class IDPDiscoveryService(SamlResource):
1090

    
1091
    isLeaf = True
1092

    
1093
    def __init__(self, manager):
1094
        self.manager = manager
1095
        SamlResource.__init__(self)
1096

    
1097
    @trace
1098
    def render(self, request):
1099
        try:
1100
            css = request.args['css'][0]
1101
        except KeyError:
1102
            css = DEFAULT_CSS
1103
        # récupération de l'id du fournisseur de service à contacter
1104
        try:
1105
            idp_ident = request.args['idp_ident'][0]
1106
        except:
1107
            return SamlErrorPage(responsecode.INTERNAL_SERVER_ERROR, _("missing parameter: Identity Provider"))
1108
        # XXx FIXME : obligatoire / donner juste RelayState ?
1109
        # url de retour après authentification
1110
        try:
1111
            # https%3A%2F%2Fbe1d.ac-dijon.fr%2Fsso%2FSSO%3FSPEntityID%3Durn%253Afi%253Aac-dijon%253Aet-bb.in.ac-dijon.fr%253A1.0%26TARGET%3Dhttps%253A%252F%252Fbb.in.ac-dijon.fr%253A8443%252Floggedin
1112
            return_url = request.args['return_url'][0]
1113
        except:
1114
            return_url = ""
1115
        # recherche d'un éventuel index pour le choix du binding
1116
        endpoint_index = None
1117
        if 'index' in request.args:
1118
            endpoint_index = request.args['index'][0]
1119
        # recherche des urls de réception pour la requête d'auth.
1120
        must_sign_request = False
1121
        try:
1122
            sp_meta = self.manager.get_metadata(idp_ident)
1123
            idp_ident = sp_meta.get('entityID', idp_ident)
1124
            #must_sign_request = is_true(sp_meta['IDPSSODescriptor'].get('WantAuthnRequestsSigned', 'false'))
1125
            #must_sign_request = is_true(sp_meta['SPSSODescriptor'].get('WantAuthnRequestsSigned', 'false'))
1126
            binding, service_url, consumer_url = get_endpoint(sp_meta, 'SingleSignOnService',
1127
                                                              ent_type='IDPSSODescriptor', allowed_bindings=[samlp.BINDING_HTTP_REDIRECT, samlp.BINDING_HTTP_POST],
1128
                                                              index=endpoint_index)
1129
        except:
1130
            err_msg = _('no usable endpoint for {0}').format(idp_ident)
1131
            return SamlErrorPage(responsecode.INTERNAL_SERVER_ERROR, err_msg)
1132
        # si ce fournisseur d'identité est désactivé, on n'envoie pas de requête
1133
        # on regarde si la fédération est autorisée pour ce partenaire
1134
        if not self.manager.check_federation_allowed(idp_ident):
1135
            return SamlErrorPage(responsecode.UNAUTHORIZED, "%s : %s" % (idp_ident, _('no federation allowed for this entity')))
1136
        # récupération de l'index du jeu d'attributs associé
1137
        attr_service_index = self.manager.get_attribute_service_index(idp_ident)
1138
        # stockage de l'id de requête pour vérification du message de réponse du FI (in_reponse_to)
1139
        request_id = self.manager.gen_saml_id({'type':'SamlRequest'})
1140
        # récupération des eventuelles options pour ce fournisseur d'identité
1141
        idp_options = self.manager.get_federation_options(idp_ident)
1142
        force_auth = is_true(idp_options.get('force_auth', 'false'))
1143
        is_passive = is_true(idp_options.get('passive', 'false'))
1144
        must_sign_request = idp_options.get('sign_request', None) or must_sign_request
1145
        must_sign_request = False
1146
        # le niveau d'authentification demandé par défaut est : >= mot de passe protégé (https)
1147
        comparison = idp_options.get('comparison', 'minimum')
1148
        class_ref = idp_options.get('req_context', saml_message.URN_PROTECTED_PASSWORD)
1149
        # génération d'une requête d'authentification
1150
        sign_method, saml_request = saml_message.gen_request(self.manager, request_id, IDP_IDENTITY,
1151
                                                             idp_ident, consumer_url, attr_service_index, CERTFILE,
1152
                                                             sign_request=must_sign_request,
1153
                                                             is_passive=is_passive, force_auth=force_auth,
1154
                                                             comparison=comparison, class_ref=class_ref)
1155
        # on stocke l'adresse de destination après authentification
1156
        if request.args.has_key('show') and DEBUG_LOG:
1157
            resp = http.Response(stream = saml_request.encode(encoding))
1158
            return self.set_headers(resp, {'Content-type': ('application/xml',)})
1159
        if return_url:
1160
            # on génère un relay_state opaque pour retrouver l'url de retour
1161
            # (permet d'utiliser des adresses de taille > 80 caractères)
1162
            relay_state = self.manager.gen_relay_state(return_url)
1163
            # on conserve le lien dans le message saml pour validation au traitement de la réponse
1164
            self.manager.update_saml_msg(request_id, {'relay_state':relay_state})
1165
        else:
1166
            relay_state = None
1167
        if binding == samlp.BINDING_HTTP_REDIRECT:
1168
            # REDIRECT response
1169
            saml_request = encode_request(saml_request, True)
1170
            if must_sign_request:
1171
                req_args = sign_request(CERTFILE, sign_method, 'SAMLRequest', saml_request, relay_state)
1172
                if req_args != []:
1173
                    redirect_url = '%s?%s' % (consumer_url, urllib.urlencode(req_args))
1174
                else:
1175
                    redirect_url = consumer_url
1176
            else:
1177
                req_args = []
1178
                req_args.append(('SAMLRequest', saml_request))
1179
                if relay_state:
1180
                    req_args.append(('RelayState', relay_state))
1181
                redirect_url = '%s?%s' % (consumer_url, urllib.urlencode(req_args))
1182
            resp = RedirectResponse(redirect_url)
1183
        else:
1184
            # Réponse avec le binding POST (default fallback)
1185
            saml_request = encode_request(saml_request)
1186
            sign_method_info = ""
1187
            relay_info = ""
1188
            if relay_state:
1189
                relay_info = """
1190
                    <input type="hidden" xx name="RelayState" value="%s">""" % relay_state
1191
            if must_sign_request:
1192
                sign_method_info = """
1193
                    <input type="hidden" xx name="SigAlg" value="%s">""" % sign_method
1194
            form = """%s<br/>
1195
                    <FORM name="form_request" action="%s" method="POST">
1196
                    <input type="hidden" name="SAMLRequest" value="%s">%s%s
1197
                    <script type="text/javascript">document.form_request.submit();</script>
1198
                    </FORM>
1199
            """ % (_('Contacting authentication server'), consumer_url, split_form_arg(saml_request), sign_method_info, relay_info)
1200
            resp = self.set_headers(http.Response(stream = gen_page(_('IDPDiscovery'), form, css)))
1201
        log.msg(_("Sending SAML authentication request to {0} ({1}) : {2}").format(idp_ident, consumer_url, request_id))
1202
        return resp