Projet

Général

Profil

cas_resources.py

Bruno Boiget, 28/11/2016 11:07

Télécharger (61,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
# cas_resources.py
12
#
13
# resssources pour le formulaire d'authentification du serveur EoleSSO
14
#
15
# compatible avec les versions 1 et 2 de la librairie CAS
16
# (http://www.ja-sig.org/products/cas/)
17
#
18
###########################################################################
19

    
20
import re
21
import os
22
import urlparse
23
from urllib2 import urlopen
24
import urllib
25
import SOAPpy
26
import traceback
27
from cgi import escape, parse_qsl
28
try:
29
    import json
30
    json_dump = json.dumps
31
except:
32
    import simplejson as json
33
    if not hasattr(json, 'dumps'):
34
        # ancienne version, write est 'deprecated' dans les versions récentes
35
        json_dump = json.write
36
    else:
37
        json_dump = json.dumps
38

    
39
from twisted.web2 import static, http, responsecode
40
from twisted.web2.resource import PostableResource as Resource
41
from twisted.web2.http_headers import Cookie as TwCookie, MimeType
42
from twisted.web2.xmlrpc import XMLRPC
43

    
44
# Imports pour la gestion de SSL via M2Crypto
45
from M2Crypto import SSL
46

    
47
# Imports EoleSSO
48
from eolesso.util import *
49
from eolesso.libsecure import ClientContextFactory, getPageM2
50
from eolesso.errors import (Redirect, CasError, MissingParameters,
51
                            InvalidTicket, InternalError)
52

    
53
import config
54
from page import gen_page, trace, log
55
from authserver import SSOSessionManager
56

    
57
# OPENID CONNECT / FRANCE CONNECT
58
import oidc_resources
59
# SAML
60
import saml_resources, saml_message, saml_utils
61

    
62

    
63
# Lecture des templates de la page d'authentification
64
AUTH_FORM = open(os.path.join('interface', 'authform.tmpl')).read()
65
# SECURID
66
if config.USE_SECURID or config.SECURID_PROVIDER:
67
    from securid_utils import SecuridCheck, SECURID_AUTH_FORM
68
else:
69
    SECURID_AUTH_FORM = ""
70

    
71
# initialisation d'un contexte client permettant de vérifier la validité
72
# des certificat (mode proxy)
73
client_ctx = ClientContextFactory(certfile = config.CERTFILE, keyfile = config.KEYFILE,
74
                           mode = SSL.m2.SSL_VERIFY_PEER|SSL.m2.SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
75
                           ca_location = config.CA_LOCATION)
76

    
77
class XMLRPCManager(XMLRPC):
78

    
79
    def __init__(self,session_manager):
80
        self.manager = session_manager
81
        XMLRPC.__init__(self)
82

    
83
    def xmlrpc_reload_configuration(self, post_data):
84
        log.msg('* {0}'.format(_('reloading server configuration')))
85
        return self.manager.reload_conf()
86

    
87
    def xmlrpc_authenticate(self, post_data, username, password, search_branch):
88
        log.msg('* xmlrpc authenticate')
89
        return self.manager.authenticate(username, password, search_branch)
90

    
91
    def xmlrpc_validate_session(self, post_data, session_id):
92
        log.msg('* xmlrpc validate_session')
93
        return self.manager.validate_session(session_id)
94

    
95
    def xmlrpc_verify_session_id(self, post_data, session_id):
96
        log.msg('* xmlrpc verify_session_id')
97
        return self.manager.verify_session_id(session_id)
98

    
99
    def xmlrpc_logout(self, post_data, session_id):
100
        log.msg('* xmlrpc logout')
101
        return self.manager.logout(session_id)
102

    
103
    def xmlrpc_verify_app_ticket(self, post_data, app_ticket, appurl):
104
        log.msg('* xmlrpc verify_app_ticket')
105
        return self.manager.verify_app_ticket(app_ticket, appurl)
106

    
107
    def xmlrpc_get_user_details(self, post_data, app_ticket, appurl, sections=False, renew=False):
108
        log.msg('* xmlrpc get_user_details')
109
        return self.manager.get_user_details(app_ticket, appurl, sections, renew)
110

    
111
    def xmlrpc_get_user_info(self, post_data, session_id, details=False):
112
        log.msg('* xmlrpc get_user_infos')
113
        return self.manager.get_user_info(session_id, details)
114

    
115
    def xmlrpc_get_auth_class(self, post_data, app_ticket):
116
        log.msg('* xmlrpc get_auth_class')
117
        return self.manager.get_auth_class(app_ticket)
118

    
119
    def xmlrpc_get_auth_instant(self, post_data, app_ticket):
120
        log.msg('* xmlrpc get_auth_instant')
121
        return self.manager.get_auth_instant(app_ticket)
122

    
123
    def xmlrpc_check_securid_login(self, post_data, securid_login):
124
        # fonction de détection des utilisateurs enregistrés pour le mode Securid
125
        return self.manager.securid_user_store.check_securid_login(securid_login)
126

    
127

    
128
class CasResource(Resource):
129
    content_type = 'text/html; charset=utf-8'
130
    required_parameters = ()
131
    headers = http_headers
132

    
133
    def set_headers(self, resp, user_headers={}):
134
        """attache à la réponse HTTP les headers par défaut
135
        + les éventuels headers passés en paramètre
136
        """
137
        if self.content_type:
138
            resp.headers.setRawHeaders('Content-type', [self.content_type])
139
        for headers in (self.headers, user_headers):
140
            for h_name, h_values in headers.items():
141
                resp.headers.setRawHeaders(h_name, h_values)
142
        return resp
143

    
144
    def render(self, request):
145
        headers = {'Content-type': MimeType.fromString(self.content_type)}
146
        errheaders = {'Content-type': MimeType.fromString('application/xml')}
147
        try:
148
            self.check_required_parameters(request)
149
            return self._render(request)
150
        except Redirect, exc:
151
            traceback.print_exc()
152
            location = exc.location
153
            headers['location'] =  location
154
            log.msg(_("Redirecting to "), location)
155
            # 303 See other
156
            return http.Response(code=303, headers=headers)
157
        except CasError, exc:
158
            traceback.print_exc()
159
            return http.Response(stream=exc.as_xml(), headers=errheaders)
160
        except Exception, exc: # unknown error
161
            traceback.print_exc()
162
            log.msg(_("An error occured while rendering content for {0} : ").format(str(request.path)) , str(exc))
163
            error = InternalError("Erreur interne rencontrée, voir les logs du serveur pour plus de détails")
164
            return http.Response(stream=error.as_xml(), headers=errheaders)
165

    
166
    def check_required_parameters(self, request):
167
        """raises a `MissingParameters` exception if all required parameters
168
        are not found in `request.args`.
169
        """
170
        missing = [param for param in self.required_parameters
171
                   if param not in request.args]
172
        if missing:
173
            raise MissingParameters(missing)
174

    
175
class CASCompliantResponse(CasResource):
176
    """implémentation du protocole CAS pour la validation de ticket
177
    """
178

    
179
    content_type = 'application/xml'
180

    
181
    authorized_codes=[responsecode.OK,
182
                      responsecode.MULTIPLE_CHOICE,
183
                      responsecode.MOVED_PERMANENTLY,
184
                      responsecode.FOUND,
185
                      responsecode.SEE_OTHER,
186
                      responsecode.NOT_MODIFIED,
187
                      responsecode.USE_PROXY,
188
                      responsecode.TEMPORARY_REDIRECT,
189
                      ]
190

    
191
    required_parameters = ('service','ticket')
192

    
193
    def __init__(self, manager, serve_proxy = False):
194
        self.manager = manager
195
        self.serve_proxy = serve_proxy
196
        self.serve_saml=False
197
        super(CASCompliantResponse, self).__init__()
198

    
199
    def _gen_user(self, infos, data_filter):
200
        """génération de la partie données utilisateur de la réponse CAS
201
        """
202
        data = ""
203
        if data_filter != "":
204
            if self.serve_saml:
205
                # génération des attributs au format SAML 1
206
                for attrs in data_filter.values():
207
                    for name, data_key in attrs.items():
208
                        if infos[data_key]:
209
                            data += """        <Attribute AttributeName="%s" AttributeNamespace="http://www.ja-sig.org/products/cas/">\n""" % name
210
                            for val in infos[data_key]:
211
                                if type(val) not in (str, unicode):
212
                                    val = str(val)
213
                                data += """          <AttributeValue>%s</AttributeValue>\n""" % escape(val)
214
                            data += """        </Attribute>\n"""
215
            else:
216
                # format xml CAS
217
                for section, attrs in data_filter.items():
218
                    if section == "default":
219
                        # pas de section, attributs au niveau le plus bas
220
                        data += "%s\n" % (self._gen_section(attrs, infos))
221
                    else:
222
                        data += "<cas:%s>\n%s    </cas:%s>\n" % (section, self._gen_section(attrs, infos), section)
223
                    data = data.rstrip()
224
        else:
225
            if self.serve_saml:
226
                # mode saml :pas d'attributs supplémentaires (user est déjà défini dans l'assertion)
227
                data = ""
228
            else:
229
                data = "    <cas:user>%s</cas:user>" % infos['uid'][0]
230
        return data
231

    
232
    def _gen_section(self,attrs,infos):
233
        """génère une section dans la réponse
234
        """
235
        section = ""
236
        for name, datakey in attrs.items():
237
            for val in infos[datakey]:
238
                if type(val) not in (str, unicode):
239
                    val = str(val)
240
                section += "      <cas:%s>%s</cas:%s>\n" % (name, escape(val), name)
241
        return section
242

    
243
    @trace
244
    def check_proxy_callback(self, cb_url, orig_parameters):
245
        """vérifie la validité d'une url de callback founie par un service proxy:
246
        - l'url doit être en https
247
        - le certificat SSL doit être valide
248
        - le nom du certificat doit correspondre au service
249
        """
250
        proxy_ticket = orig_parameters[3]
251
        if self.manager._DBAppSessionFromTicket(proxy_ticket):
252
        #if proxy_ticket in self.manager.app_sessions:
253
            ticket = self.manager.app_sessions[proxy_ticket]
254
            log_prefix = "%s -- " % ticket.session_id
255
        else:
256
            log_prefix = ""
257
        # recherche de la session utilisateur associée à la demande
258
        log.msg("%s%s" % (log_prefix, _('PGT asked for service {0}').format(orig_parameters[2])))
259
        url = urlparse.urlparse(cb_url)
260
        if url.scheme == 'https':
261
            # connexion à l'url de callback pour vérifier le certificat (port 443 si pas de port spécifié)
262
            d = getPageM2(cb_url, checker = client_ctx._cert_verify, contextFactory = client_ctx, timeout = 10)
263
            return d.addCallbacks(self.proxy_cert_ok, self.proxy_errb, callbackArgs = [client_ctx, orig_parameters, url], errbackArgs = [orig_parameters,url])
264
        # url de proxy non valide
265
        log.msg(_("!! Invalid proxy url (https is mandatory) !!"))
266
        return self.validate_st(orig_parameters)
267

    
268
    @trace
269
    def proxy_cert_ok(self, stream, ctx, orig_parameters, url):
270
        """gestion de la demande de PGT si proxy valide
271
        - envoi d'une requête GET sur l'url de callback du proxy avec les paramètres pgtId et pgtIou
272
        - vérification du retour : OK si code 200 (ou 3xx si redirection)
273
        - la validation du ticket d'origine (ST ou PT) continue, avec ajout de pgtIou si vérification OK
274
        """
275
        service = orig_parameters[2]
276
        ticket = orig_parameters[3]
277
        service_host = url.hostname
278
        # vérification de la concordance service / certificat
279
        service_host = config.ALTERNATE_IPS.get(service_host, service_host)
280
        if not is_dn_in_hostname(ctx.peer_cert_data['subject'], service_host):
281
            log.msg(_("!! Certificate error : certificate name does not match service !!"))
282
            # XXX FIXME : remettre en place la validation
283
        else:
284
            pgt_iou = None
285
            # Création d'un pgt temporaire (et pgtiou associé) en attendant la validation ?
286
            app_ticket = self.manager.get_proxy_granting_ticket(ticket, service, url)
287
            if app_ticket is not None:
288
                # préparation de la requête à envoyer sur le callback du proxy
289
                # ajout de pgtId et pgtIou dans les arguments
290
                callb_url, callb_args = urllib.splitquery(url.geturl())
291
                if callb_args:
292
                    req_args = parse_qsl(callb_args)
293
                else:
294
                    req_args = []
295
                req_args.append(('pgtId', app_ticket.pgt))
296
                req_args.append(('pgtIou', app_ticket.pgtiou))
297
                # si besoin, on déclare un proxy http pour y accéder
298
                self.manager.set_urllib_proxy(app_ticket)
299
                # envoi de la requête et lecture de la réponse
300
                u = urlopen("%s?%s" % (callb_url, urllib.urlencode(req_args)))
301
                data = u.read()
302
                self.manager.set_urllib_proxy()
303
                if u.code in self.authorized_codes:
304
                    log.msg(_("** Proxy response code OK : {0}").format(u.code))
305
                    # le proxy doit retourner un code 200 ou 3xx après envoi du pgtId et pg_tiou
306
                    return self.validate_st(orig_parameters, app_ticket)
307
                # réponse du proxy invalide, on n'envoie pas de pgt iou à la validation du ticket
308
                log.msg(_("** Proxy response code ERROR : {0}").format(u.code))
309
            else:
310
                log.msg(_("!! Unable to obtain PGT for service {0} !!").format(service))
311
        return self.validate_st(orig_parameters)
312

    
313
    @trace
314
    def proxy_errb(self, err, orig_parameters, url):
315
        log.msg(_("!! Proxy rejected : {0} !!").format(url.geturl()))
316
        log.msg("\n!! --> %s !!\n" % err.getErrorMessage())
317
        return self.validate_st(orig_parameters)
318

    
319
    def _render(self, request):
320
        error = {'code':'INTERNAL_ERROR','detail':''}
321
        data = error, None
322
        ok = False
323

    
324
        from_url = request.args['service'][0]
325
        ticket = request.args['ticket'][0]
326
        if not self.serve_proxy and ticket.startswith('PT'):
327
            raise InvalidTicket(ticket, _("Received PT ticket instead of ST"))
328
        # Récupération du paramètre renew si présent
329
        if request.args.get('renew', None) is not None:
330
            renew = True
331
        else:
332
            renew = False
333
        # si validation par messages SAML 1.1, on récupère la requête
334
        if request.args.get('Request', None):
335
            saml_request = request.args['Request'][0]
336
        # CAS 2 : récupération de l'url de callback pour les proxys (pgtURl : optionnel)
337
        pgt_url = None
338
        if config.CAS_VERSION == 2:
339
            try:
340
                pgt_url = request.args['pgtUrl'][0]
341
                return self.check_proxy_callback(pgt_url, [ok, data, from_url, ticket, renew])
342
            except KeyError:
343
                # Cas sans pgtUrl, on continue la validation du ticket normalement
344
                pass
345
        return self.validate_st([ok, data, from_url, ticket, renew])
346

    
347
    @trace
348
    def _traversed_proxies(self, ticket):
349
        """return the <cas:proxies> node listing traversed proxies
350

351
        NOTE: /serviceValidate and /proxyValidate could share the same
352
        implementation since ticket.proxypath() should return an empty
353
        list in case of ST apptickets, but just in case, we return
354
        an hard-coded empty node for ST tickets
355
        """
356
        proxies = '\n'.join('    <cas:proxy>%s</cas:proxy>' % url
357
                            for url in ticket.proxypath())
358
        return u'\n  <cas:proxies>\n%s\n  </cas:proxies>' % proxies
359

    
360
    @trace
361
    def validate_st(self, validate_parameters, pgt=None):
362
        ok, data, from_url, ticket, renew = validate_parameters
363
        if ticket is not None:
364
            if pgt is not None:
365
                pgt_data = "\n  <cas:proxyGrantingTicket>%s</cas:proxyGrantingTicket>" % pgt.pgtiou
366
                # on renvoie les proxys traversés si besoin
367
                if pgt.proxypath() != []:
368
                    pgt_data += self._traversed_proxies(pgt)
369
            else:
370
                # si on n'a pas de paramètre pgt_iou, on considère que d'éventuels tests
371
                # sur une url de proxy ont échoué
372
                self.manager.invalidate_proxy(ticket)
373
                pgt_data = ""
374
            # si ticket inconnu du serveur, on va tenter sa validation sur tous les serveurs d'auth
375
            ok, data = self.manager.get_user_details(ticket, from_url, True, renew)
376
        infos, filter_data = data
377
        if ok:
378
            # authentification distante (SSO) reussie
379
            # génération des informations utilisateur
380
            user_infos = self._gen_user(infos, filter_data)
381
            # formatting response
382
            if self.serve_saml:
383
                # on récupère la date d'authentification de l'utilisateur
384
                auth_instant = self.manager.get_auth_instant(ticket)
385
                # Réponse au format SAML 1.1
386
                response = saml_message.gen_saml11_response(ticket, auth_instant, from_url, config.IDP_IDENTITY, infos['uid'][0], user_infos)
387
                response = response.encode(config.encoding)
388
            elif config.CAS_VERSION == 2:
389
                # Réponse CAS v2
390
                resp_body = user_infos + pgt_data
391
                response = cas_response('authenticationSuccess', resp_body)
392
            else:
393
                # Réponse CAS v1
394
                response = 'yes\n%s\r\n\r\n' % infos['uid'][0]
395
        else:
396
            error, filter_data  = data
397
            # aucun des serveurs n'a authentifié l'utilisateur (meme l'authentification locale), on revient
398
            # sur la page de saisie de username/password
399
            if self.serve_saml and error['code'] != 'INTERNAL ERROR':
400
                # issue_instant, recipient, response_id
401
                response = saml_message.gen_saml11_error(error, from_url)
402
                response = response.encode(config.encoding)
403
            elif config.CAS_VERSION == 2 or self.serve_saml:
404
                response = cas_response('authenticationFailure', escape(error['detail']), code=error['code'])
405
            else:
406
                response = 'no\r\n\r\n'
407
        if config.CAS_VERSION == 1 and not self.serve_saml:
408
            self.content_type = 'text/html; charset=utf-8'
409
        cas_resp = http.Response(stream=response)
410
        if self.serve_saml:
411
            # SOAP additionnal headers for SAML 1.1
412
            self.set_headers(cas_resp, {'SOAPAction':['http://www.oasis-open.org/committees/security']})
413
        else:
414
            self.set_headers(cas_resp)
415
        return cas_resp
416

    
417
class SAMLCompliantResponse(CASCompliantResponse):
418

    
419
    content_type = 'application/xml'
420
    required_parameters = ('TARGET',)
421

    
422
    def __init__(self, manager, serve_proxy = False):
423
        super(SAMLCompliantResponse, self).__init__(manager, serve_proxy)
424
        self.serve_saml = True
425
        # on utilise render pour renderHTTP
426
        self.renderHTTP = super(SAMLCompliantResponse, self).render
427

    
428
    def _render(self, request):
429
        target = request.args['TARGET'][0]
430
        # Récupération de l'artefact dans la requête SAML (correspond au ticket à valider).
431
        return request.stream.read().addCallbacks(self.process_saml11_request, log.msg, callbackArgs = [target])
432

    
433
    def process_saml11_request(self, xml_stream, target):
434
        """procceses a saml1 request and extracts Artifact value
435
        """
436
        error = {'code':'INTERNAL_ERROR','detail':''}
437
        # extraction de la requête SAML1
438
        try:
439
            artifact = saml_utils.get_saml11_artifact(xml_stream)
440
        except SOAPpy.Error:
441
            # erreur de parsing de l'entête SOAP, on retourne une erreur
442
            error = {'code':'INTERNAL ERROR', 'detail':_('Invalid SOAP Headers')}
443
            artifact = None
444
        except:
445
            error = {'code':'INTERNAL ERROR', 'detail':_('No ticket in SAML Request')}
446
            artifact = None
447
        data = error, None
448
        return self.validate_st([False, data, target, artifact, False])
449

    
450
class ProxyResource(CasResource):
451
    """
452
    page de génération d'un ticket de type PT
453
    """
454
    addSlash = False
455
    content_type = 'application/xml'
456

    
457
    def __init__(self, manager):
458
        self.manager = manager
459
        super(ProxyResource, self).__init__()
460

    
461
    def _render(self, request):
462
        try:
463
            pgt = request.args['pgt'][0]
464
            target_service = request.args['targetService'][0]
465
        except:
466
            error = {'code':'INVALID_REQUEST','detail':_("Missing parameter")}
467
        # vérification du pgt
468
        if pgt in self.manager.proxy_granting_sessions:
469
            # génération du ticket PT
470
            ticket = self.manager.get_proxy_ticket(pgt, target_service)
471
            response_body = "<cas:proxyTicket>%s</cas:proxyTicket>" % ticket
472
            response = cas_response('proxySuccess', response_body)
473
            return self.set_headers(http.Response(stream=response))
474
        else:
475
            error = {'code':'BAD_PGT', 'detail':_("PGT not recognized by server : {0}").format(pgt)}
476
        response = cas_response('proxyFailure', escape(error['detail']), code=error['code'])
477
        return self.set_headers(http.Response(stream=response))
478

    
479

    
480
class LoggedInForm(CasResource):
481
    """
482
    page de confirmation d'ouverture de session (si pas d'url de redirection fournie)
483
    """
484
    addSlash = False
485

    
486
    def __init__(self, manager):
487
        self.manager = manager
488
        super(LoggedInForm, self).__init__()
489

    
490
    def _render(self,request):
491
        try:
492
            css = escape(request.args['css'][0])
493
        except KeyError:
494
            css = config.DEFAULT_CSS
495
        sso_cookie = getCookie(request, 'EoleSSOServer')
496
        if sso_cookie:
497
            user_session = sso_cookie.value
498
            sessions = ""
499
            local_sessions = []
500
            for app_t in self.manager.user_app_tickets[user_session]:
501
                if hasattr(app_t, 'provider_data'):
502
                    sessions += """<li>Identité fournie par : %s</li>""" % \
503
                            self.manager.external_providers[app_t.provider_data['provider']][2]
504
                elif hasattr(app_t,'saml_ident'):
505
                    sessions += """<li>session distante sur %s</li>""" % app_t.saml_ident
506
                else:
507
                    if app_t.service_url not in local_sessions:
508
                        local_sessions.append(urllib.quote(app_t.service_url, safe=uri_reserved_chars))
509
            if local_sessions:
510
                sessions += "<li>%s</li>" % "</li><li>".join(local_sessions)
511
            if sessions:
512
                content = """<h1>Liste de vos sessions ouvertes :</h1><br/>
513
                <ul>%s</ul>""" % sessions
514
            else:
515
                content = """<h1>Aucune session d'application trouvée</h1>"""
516
            content += """<form action="/logout" method="post">
517
            <p class=formvalidation><input class="btn" type="Submit" value="%s">
518
            </p></form>""" % _("Disconnect")
519
        else:
520
            content = _('Invalid session')
521
        response = http.Response(stream=gen_page(_("Valid SSO session"), content, css))
522
        return self.set_headers(response)
523

    
524
class ErrorPage(CasResource):
525
    """
526
    page d'erreur http
527
    """
528
    addSlash = False
529

    
530
    def __init__(self, code, description):
531
        self.code = code
532
        self.description = description
533
        title = (_("EoleSSO : Error"))
534
        super(ErrorPage, self).__init__()
535

    
536
    def _render(self,request):
537
        return http.StatusResponse(code=self.code, description=self.description)
538

    
539
class UnauthorizedService(ErrorPage):
540
    """
541
    Page affichée en cas de refus d'envoi d'une réponse CAS à un service
542
    renvoie une erreur http UNAUTHORIZED par défaut.
543
    Il est possible d'afficher une page personnalisée à la place en créant un template
544
    /usr/share/sso/interface/invalid_service.tmpl (fichier d'exemple dans le répertoire)
545
    """
546
    def __init__(self):
547
        tmpl_file = os.path.join(config.SSO_PATH, 'interface', 'unauthorized_service.tmpl')
548
        if os.path.isfile(tmpl_file):
549
            self.title = _("EoleSSO : Error")
550
            self.tmpl_data = file(tmpl_file).read()
551
        else:
552
            self.tmpl_data = None
553
            description = _('unauthorized service url : {0}')
554
            ErrorPage.__init__(self, responsecode.UNAUTHORIZED, description)
555

    
556
    def _render(self,request):
557
        from_url = escape(request.args['service'][0])
558
        if self.tmpl_data:
559
            css = config.DEFAULT_CSS
560
            try:
561
                css = escape(request.args['css'][0])
562
            except KeyError:
563
                pass
564
            response = http.Response(stream=gen_page(self.title, self.tmpl_data.format(from_url), css))
565
            return self.set_headers(response)
566
        else:
567
            return http.StatusResponse(code=self.code, description=self.description.format(from_url))
568

    
569
class LogoutForm(CasResource):
570
    """
571
    page de confirmation de logout (si pas d'url de redirection fournie)
572
    """
573
    addSlash = False
574

    
575
    def __init__(self, manager):
576
        self.manager = manager
577
        super(LogoutForm, self).__init__()
578

    
579
    def _render(self,request):
580
        # invalidation de la session sur le serveur d'authentification
581
        cookies = getCookies(request)
582
        for cookie in cookies:
583
            if cookie.name == 'EoleSSOServer':
584
                session_id = cookie.value
585
                if session_id is not None:
586
                    # suppression de la session sur le manager
587
                    self.manager.logout(session_id.encode(config.encoding))
588
                    cookie.expires = 1
589
                break
590
        try:
591
            css = escape(request.args['css'][0])
592
        except KeyError:
593
            css = config.DEFAULT_CSS
594
        if request.args.has_key('service'):
595
            url_from = request.args['service'][0]
596
            response = RedirectResponse(url_from)
597
            log.msg(_("Logged out : redirecting to {0}").format(url_from))
598
        else:
599
            #si pas de parametre from, on affiche la page de logout en local
600
            content = """<form action="/login" method="post">
601
            <p class=formvalidation>
602
            <input class="btn" type="Submit" value="%s"></p></form>""" % _("Connect")
603
            response = http.Response(stream=gen_page(_('SSO session closed'), content, css))
604
            self.set_headers(response)
605
        # mise en place des cookies
606
        if cookies:
607
            response.headers.setHeader('Set-Cookie', cookies)
608
        return response
609

    
610
class UserChecker(Resource):
611
    """ressource utilisée pour détecter certains aspects de l'utilisateur
612
    - vérifie si l'utilisateur est sujet à des problèmes de doublon (annuaire multi établissement)
613
    - Si l'accès OTP est configuré, vérifie si l'utilisateur a enregistré son identifiant OTP
614
    """
615

    
616
    isLeaf = True
617
    content_type = 'application/json; charset=utf-8'
618

    
619
    def __init__(self, manager):
620

    
621
        self.manager = manager
622
        super(UserChecker, self).__init__()
623

    
624
    def render(self, request):
625
        """méthode de rendu http (renvoie si un utilisateur est déjà enregistré)
626
        Utilisé par le fomulaire depuis une requête AJAX
627
        le nom d'utilisateur à vérifier doit être passé dans la requête (argument username)
628
        """
629
        username = ''
630
        current_branch = request.args.get('user_branch', ['default'])[0]
631
        if 'username' in request.args:
632
            username = request.args.get('username', [''])[0]
633
            # vérification des doublons
634
            if self.manager._data_proxy.use_branches:
635
                if 'check_branches' in request.args and is_true(request.args.get('check_branches', [''])[0]):
636
                    defer_branches = self.manager._data_proxy.get_user_branches(username)
637
                    return defer_branches.addCallbacks(self.callb_render, log.msg, callbackArgs=[username, current_branch])
638
        return self.callb_render([], username, current_branch)
639

    
640
    @trace
641
    def callb_render(self, search_branches, username, current_branch):
642
        #search_branches = []
643
        #for branche in branches:
644
        #    br_dn = str(branche)
645
        #    for search_br, libelle in self.manager._data_proxy.search_branches.items():
646
        #        if search_br in br_dn:
647
        #            search_branches.append((br_dn, libelle))
648
        #            break
649
        # tri de la liste sur le libellé établissement
650
        search_branches.sort(key  = lambda branche: branche[1])
651
        user_infos = {'search_branches':search_branches}
652
        if username and self.manager.securid_user_store:
653
            # vérification de l'enregistrement de login OTP
654
            otp_login = self.manager._data_proxy.check_otp_config(current_branch)
655
            if otp_login == "identiques":
656
                # les logins sont identiques au login LDAP pour cet annuaire
657
                user_infos['securid_registered'] = 'true'
658
            elif otp_login == "configurables":
659
                user_infos['securid_registered'] = self.manager.securid_user_store.check_registered(username, current_branch)
660
        response = http.Response(code=responsecode.OK, stream=json_dump(user_infos))
661
        response.headers.setRawHeaders('Content-type', [self.content_type])
662
        return response
663

    
664
class LocalCookie(CasResource):
665
    """
666
    page de génération d'un cookie d'authentification local
667
    """
668
    addSlash = False
669
    required_parameters = ('return_url','ticket')
670

    
671
    def __init__(self, manager):
672
        self.manager = manager
673
        super(LocalCookie, self).__init__()
674

    
675
    def _render(self, request):
676
        try:
677
            ticket = request.args['ticket'][0]
678
            return_url = request.args['return_url'][0]
679
        except:
680
            error = {'code':'INVALID_REQUEST','detail':_("Missing parameter")}
681
            log.msg(error)
682
        log.msg(_("Local cookie requested"))
683
        # on vérifie que la session est bien existante et valide
684
        response = RedirectResponse(return_url)
685
        if self.manager.verify_session_id(ticket):
686
            ticket = ticket.encode(config.encoding)
687
            # XXX FIXME : gérer plusieurs niveaux de parents (redirection sur parent si ticket non local)
688
            if self.manager.user_sessions.get(ticket,(None,))[0] == None:
689
                print " not implemented : redirect one level higher !"
690
            cookies = []
691
            for cookie in getCookies(request):
692
                if cookie.name != 'EoleSSOServer':
693
                    cookies.append(cookie)
694
            cookies.append(TwCookie("EoleSSOServer", ticket, path = "/", discard=True, secure=True))
695
            if cookies:
696
                log.msg(_("Local cookie sent, redirecting to child server"))
697
                response.headers.setHeader('Set-Cookie', cookies)
698
        return response
699

    
700
class InfoEtabs(CasResource):
701
    """
702
    page de récupération des données des établissements (eole-dispatcher)
703
    """
704
    addSlash = False
705
    content_type = 'application/json'
706

    
707
    def _render(self,request):
708
        info_etabs = get_etabs(os.path.join(config.SSO_PATH,'interface','scripts','etabs.js'), sso_dir=config.SSO_PATH)
709
        response = http.Response(code=responsecode.OK, stream=json_dump(info_etabs._sections))
710
        return self.set_headers(response)
711

    
712
class LoginForm(CasResource):
713
    """The resource that is returned when you are not logged in"""
714
    addSlash = False
715

    
716
    def __init__(self, manager, auth_server, *args):
717
        self.manager = manager
718
        self.auth_server = auth_server
719

    
720
        # pré-calcul du select des établissements (si annuaire multi-établissements)
721
        self.select_etab = self.calc_select_etab()
722
        self.content = ''
723
        # utilisation de la favicon fournie dans le thème ou celle par défaut
724
        if os.path.isfile('interface/theme/image/favicon.ico'):
725
            self.favicon = 'interface/theme/image/favicon.ico'
726
        elif os.path.isfile('interface/theme/image/icon.ico'):
727
            self.favicon = 'interface/theme/image/icon.ico'
728
        else:
729
            self.favicon = 'interface/images/icon.ico'
730
        Resource.__init__(self)
731

    
732
    @trace
733
    def calc_select_etab(self):
734
        select_etab = ""
735
        if self.manager._data_proxy.use_branches:
736
            select_etab = """<tr id="row_etab" style="display:none;">
737
            <td><label for='select_etab' accesskey='e'>%s</label></td>
738
            <td><select name="select_etab" id="select_etab" onchange="check_user_options('false')">
739
            <option value=""></option>""" % _('User Origin')
740
            #for search_base, libelle in self.manager._data_proxy.search_branches.items():
741
            #    select_etab += "<option value=%s>%s</option>" % (search_base, libelle)
742
            select_etab += "</select></td></tr>"
743
        return select_etab
744

    
745
    @trace
746
    def get_css_from_service(self, from_url):
747
        css = config.DEFAULT_CSS
748
        # recherche d'une eventuelle css associée
749
        service_filter = self.manager._check_filter(from_url)[0]
750
        if service_filter not in ['default', '']:
751
            if os.path.exists(os.path.join('interface','%s.css' % service_filter)):
752
                css = service_filter
753
        return css
754

    
755
    @trace
756
    def render_content(self, request):
757
        """
758
        protocole CAS:
759
         1. service : l'URL d'où on vient
760
         2. renew=true : on force à se réauthentifier
761
         3. gateway=true : si pas authentifié, on revient à l'URL
762
            définie par service sans ticket d'application
763
        ajout Eole
764
         4. redirect_to : permet de définir une page ou revenir après authentification
765
            (utile pour l'authentification depuis des protocoles non CAS)
766
        """
767
        return self.render_login(request, request.args)
768

    
769
    @trace
770
    def render_login(self, request, request_args, provider_data=None):
771
        """
772
        request args : arguments de la requête actuelle ou arguments de la requête
773
                       originale si retour d'une authentification externe.
774
        provider_data: information supplémentaires si l'utilisateur a été authentifié
775
                       par un fournisseur d'identité déclaré.
776
        """
777
        self.content = ""
778
        gateway = is_true(request_args.get('gateway', ['false'])[0])
779
        renew = is_true(request_args.get('renew', ['false'])[0])
780
        warn = is_true(request_args.get('warn', ['false'])[0])
781
        css = config.DEFAULT_CSS
782
        try:
783
            from_url = request_args['service'][0]
784
            css = self.get_css_from_service(from_url)
785
        except KeyError, e:
786
            # nécessaire car les ressources ne sont instanciées qu'une
787
            # seule fois
788
            from_url = ''
789
        try:
790
            css = escape(request_args['css'][0])
791
        except KeyError:
792
            pass
793
        try:
794
            redirect_to = request_args['redirect_to'][0]
795
        except KeyError:
796
            redirect_to = ''
797
        # si demandé, on vérifie que l'application de destination est reconnue (filtre défini)
798
        if from_url and config.VERIFY_APP:
799
            if self.manager.get_app_infos(from_url) is None:
800
                return '/unauthorizedService?service=%s' % from_url
801
        # on va verifier si on a un identifiant de session dans le cookie
802
        user_session = getCookie(request, 'EoleSSOServer')
803
        # XXX: IE et Opera ne gerent pas les cookies de la meme
804
        # maniere que Firefox sous firefox, quand un cookie devient
805
        # invalide (au niveau du temps), il est supprimé sous IE et
806
        # Opera, ils sont gardés, on doit donc tester si ils ont une
807
        # valeur spéciale, ici foo
808
        # si on en a bien un, on le verifie auprès du serveur d'authentification
809
        if not renew and user_session and user_session != 'foo':
810
            session_id = user_session.value.encode(config.encoding)
811
            verif =  self.manager.verify_session_id(session_id)
812
            # si l'ID de session est verifie, on génère le ticket applicatif,
813
            # et on redirige vers l'URL de base
814
            if verif[0]:
815
                if from_url:
816
                    app_ticket = self.manager.get_app_ticket(session_id, from_url)
817
                    url = urljoin(from_url, 'ticket=%s' % app_ticket)
818
                    if warn:
819
                        head = """<head><script type="text/javascript">
820
function RedirectWarn() {
821
alert("%s");
822
document.location.href="%s";
823
};
824
</script></head>"""
825
                        warn_msg = _("You are being authenticated by {0}").format(config.AUTH_SERVER_ADDR)
826
                        self.content = str("""<html>%s<body onload="setTimeout(function(){RedirectWarn()}, 200);"></body></html>""" % (head % (warn_msg, urllib.quote(url, safe=uri_reserved_chars))))
827
                        return None
828
                    return url
829
                return '/loggedin'
830

    
831
        if gateway and from_url:
832
            return urljoin(from_url, 'ticket=')
833
        self.auth_msg = _("Authentication needed")
834
        if request_args.has_key('failed'):
835
            if request_args['failed'][0] == '1':
836
                if "provider" in request_args:
837
                    # specific message if failure when authenticating to an external provider
838
                    provider_label = self.manager.external_providers[request.args['provider'][0]][2]
839
                    if request_args.get('reason', [''])[0] == 'denied':
840
                        # user explicitly denied account information
841
                        self.auth_msg = _("Account information denied ({0}),<br/>log in with local account").format(provider_label)
842
                    else:
843
                        self.auth_msg = _("Error occured while authentication to {0}").format(provider_label)
844
                else:
845
                    self.auth_msg = _("Authentication failed, try again")
846

    
847
        # création d'un login ticket jouable une seule fois pour cet essai d'authentification
848
        if provider_data:
849
            # après retour depuis un FI externe, on conserve les informations qu'il a fourni
850
            login_ticket = self.manager.get_login_ticket(provider_data)
851
        else:
852
            # stockage des informations de la requête actuelle au cas où on redirigerait sur
853
            # un fournisseur d'identité externe.
854
            login_ticket = self.manager.get_login_ticket(request_args)
855
        if from_url != "":
856
            service_input = """<input type="hidden" name="service" value="%s"/>""" % urllib.quote(from_url, safe=uri_reserved_chars)
857
        else:
858
            service_input = ""
859
        if redirect_to != "":
860
            redirect_input =  """<input type="hidden" name="redirect_to" value="%s"/>""" % urllib.quote(redirect_to, safe=uri_reserved_chars)
861
        else:
862
            redirect_input = ""
863

    
864
        #############################################
865
        # Calcul du formulaire d'authentification
866
        #############################################
867
        additional_form = ""
868
        passwd_check_func = ""
869
        if config.DEBUG_LOG:
870
            autocomplete = "on"
871
        else:
872
            autocomplete = "off"
873
        if config.USE_SECURID: #  or config.SECURID_PROVIDER:
874
            additional_form += SECURID_AUTH_FORM % (_("label_unregistered"),
875
                                                    _("Securid user"), autocomplete, _("Securid user"),
876
                                                    _("OTP PIN number"), _("Current token"), autocomplete,
877
                                                    _("Current token"),
878
                                                   )
879
            passwd_check_func = 'onkeyup="checkotp()"'
880
        # avertissement legal (en dessous du cadre)
881
        theme_txt = os.path.join(config.SSO_PATH, 'interface', 'theme', 'avertissement.txt')
882
        interf_txt = os.path.join(config.SSO_PATH, 'interface', 'avertissement.txt')
883
        if os.path.exists(theme_txt):
884
            avertissement = open(theme_txt).read()
885
        elif os.path.exists(interf_txt):
886
            avertissement = open(interf_txt).read()
887
        else:
888
            avertissement = ""
889
        # message en fonction du navigateur
890
        theme_ie = os.path.join(config.SSO_PATH, 'interface', 'theme', 'alert_ie.tmpl')
891
        interf_ie = os.path.join(config.SSO_PATH, 'interface', 'alert_ie.tmpl')
892
        if os.path.exists(theme_ie):
893
            avertissement_ie = open(theme_ie).read()
894
        elif config.ACTIVER_ENVOLE_INFOS and os.path.exists('/var/www/html/envole-infos/informations_ie.php'):
895
            avertissement_ie = open(interf_ie).read() % config.POSH_URL
896
        else:
897
            avertissement_ie = ""
898
        # aide utilisateur pour le choix du login
899
        theme_login = os.path.join(config.SSO_PATH, 'interface', 'theme', 'login_help.tmpl')
900
        interf_login = os.path.join(config.SSO_PATH, 'interface', 'login_help.tmpl')
901
        if os.path.exists(theme_login):
902
            login_help = open(theme_login).read()
903
        elif os.path.exists(interf_login):
904
            login_help = open(interf_login).read()
905
        else:
906
            login_help = ""
907
        if config.DEBUG_LOG:
908
            autocomplete = 'on'
909
        else:
910
            autocomplete = 'off'
911
        provider_infos = ""
912
        if self.manager.external_providers:
913
            if provider_data:
914
                # message d'information pour l'association avec un compte externe
915
                provider_name = provider_data['provider']
916
                provider_label = self.manager.external_providers[provider_data['provider']][2]
917
                alt_msg = _("Accounting with {0}").format(provider_label)
918
                assoc_msg = '<img src="images/logo-{0}.png" alt="{1}" title="{1}">'.format(provider_name, alt_msg)
919
                assoc_msg += _("Unknown {0} account.").format(provider_label)
920
                assoc_msg += " "
921
                assoc_msg += _('Please authenticate with {0} to register it.').format(config.LOCAL_ACCOUNT_LABEL)
922
                provider_infos += """<div class="register_account">{}</div>""".format(assoc_msg)
923
            else:
924
                # génération des boutons de provider
925
                for oidc_p in self.manager.external_providers:
926
                    oid_args = (('provider', oidc_p), ('lt', login_ticket))
927
                    alt_msg = _("Connection with {0}").format(self.manager.external_providers[oidc_p][2])
928
                    about_info = self.manager.external_providers[oidc_p][3]
929
                    if about_info is not None:
930
                        about_url = """<br><a class="providerabout "href={0}>{1}</a>""".format(about_info[0], about_info[1])
931
                    else:
932
                        about_url = ""
933
                    provider_infos += '<div class="oicprovider"><a class="btn-connect" href="/oidconnect?{1}"><img src="images/{0}.png" style="border-style: none;" alt="{2}" title="{2}"></a>{3}</div>'.format(oidc_p, urllib.urlencode(oid_args), alt_msg, about_url)
934
            provider_infos += '<div class="oidc_sep" style="clear:both;"></div>'
935

    
936
        # création de la page
937
        content = AUTH_FORM % (provider_infos,
938
                login_ticket,
939
                service_input,
940
                redirect_input,
941
                _("Login"), _("Login"), autocomplete,
942
                _("Password"), _("Password"), autocomplete,
943
                passwd_check_func,
944
                self.select_etab,
945
                additional_form,
946
                login_help,
947
                _("Submit"),
948
                avertissement_ie, avertissement)
949
        # scripts dynamiques
950
        javascript = """document.forms['cas_auth_form'].username.focus();setTimeout(callb_onload, 200);"""
951
        header_script = """<script type="text/javascript" src="scripts/mootools-core-1.4.2.js"></script>
952
<script type="text/javascript" src="scripts/tools.js?v=2.0"></script>
953
<script type="text/javascript" src="scripts/etabs.js"></script>
954
<script type="text/javascript" src="scripts/homonymes.js"></script>
955
<script type="text/javascript">
956
"""
957
        otp_enabled = 'false'
958
        if config.USE_SECURID:
959
            # chargement des scripts pour gestion de la saisie de mots de passe OTP
960
            otp_enabled = 'true'
961
            header_script += open('interface/scripts/authform_otp.js').read() %\
962
            (config.OTPPASS_MINSIZE,
963
             config.OTPPASS_MAXSIZE,
964
             config.OTPPASS_REGX)
965
        header_script += open('interface/scripts/authform_ajax.js').read() %\
966
        (otp_enabled,
967
         _('label_password_OTP'),
968
         _('label_registered'),
969
         _('label_password'),
970
         _('label_unregistered'),
971
         _('label_password'),
972
         _('select your origin'),
973
         _('academic'),
974
         _('several users correspond to '),
975
         _('please select your authentication source'),
976
)
977
        header_script += open('interface/scripts/authform_ie.js').read().strip()
978
        header_script += "\n</script>"
979
        self.content = gen_page(self.auth_msg, content, css, javascript, header_script)
980
        return None
981

    
982
    @trace
983
    def render_provider(self, request):
984
        """checks user data sent by an authorized external provider
985
        and manages association with local users.
986
        """
987
        try:
988
            login_ticket = request.args['lt'][0]
989
            assert self.manager.login_sessions.validate_session(login_ticket)
990
        except:
991
            return ErrorPage(_("Authentication"),_("Access denied"))
992
        cookies = getCookies(request)
993
        # récupération de l'utilisateur associé dans le UserStore correspondant si présent,
994
        # ou demande d'authentification locale pour association. récupération des données via 'lt'
995
        provider_data = self.manager.login_sessions.get_session_info(login_ticket)
996
        provider = provider_data['provider']
997
        username = provider_data['username']
998
        orig_args = provider_data['orig_args']
999
        user_store = self.manager.external_providers[provider][1]
1000
        search_branch, local_user = user_store.get_local_user(username)
1001
        # stockage des informations sur l'utilisateur local dans provider_data
1002
        provider_data['local_user'] = local_user
1003
        provider_data['search_branch'] = search_branch
1004
        if local_user:
1005
            provider_data['need_register'] = False
1006
            # l'identifiant externe est associé à un compte local, on l'authentifie automatiquement
1007
            # détection de l'attribut de recherche défini sur l'annuaire concerné
1008
            ldap_host = search_branch.split(':', 1)[0]
1009
            fed_attr_name = "uid"
1010
            for ldap_server in self.manager._data_proxy.ldap_servers:
1011
                if ldap_server[0] == ldap_host:
1012
                    fed_attr_name = ldap_server[3]
1013
            feder_attrs = {fed_attr_name:[local_user]}
1014
            # jeu d'attributs correspondant
1015
            attr_set = {'user_attrs':{fed_attr_name:fed_attr_name}, 'branch_attrs':{}, 'optional_attrs':{}}
1016
            # ouverture de session par fédération
1017
            defer_auth = self.manager.authenticate_federated_user(feder_attrs,
1018
                                                                  provider,
1019
                                                                  time.time(),
1020
                                                                  saml_utils.available_contexts['URN_PROTECTED_PASSWORD'],
1021
                                                                  search_branch = search_branch,
1022
                                                                  attr_set=attr_set)
1023
            from_url = orig_args.get('service' , [''])[0]
1024
            redirect_to = orig_args.get('redirect_to', [''])[0]
1025
            return defer_auth.addCallback(self.callb_auth, from_url, redirect_to, cookies, provider_data)
1026
        else:
1027
            provider_data['need_register'] = True
1028
            # l'identifiant externe est inconnu, on demande à l'utilisateur de s'authentifier localement
1029
            # rendu du formulaire d'authentification avec les informations nécessaire
1030
            redirect_to = self.render_login(request, orig_args, provider_data)
1031
            return self.return_login(redirect_to)
1032

    
1033
    @trace
1034
    def render_verify(self, request):
1035
        try:
1036
            username = unicode(request.args['username'][0],config.encoding)
1037
        except:
1038
            return ErrorPage(_("Authentication"),_("Access denied"))
1039
        try:
1040
            search_branch = unicode(request.args['select_etab'][0],config.encoding).encode(config.encoding)
1041
        except:
1042
            search_branch = "default"
1043
        try:
1044
            password = unicode(request.args['password'][0],config.encoding)
1045
        except:
1046
            password = ""
1047
        try:
1048
            from_url = request.args['service'][0]
1049
        except:
1050
            from_url = ""
1051
        try:
1052
            redirect_to = request.args['redirect_to'][0]
1053
        except KeyError:
1054
            redirect_to = ''
1055
        try:
1056
            securid_pwd = request.args['securid_pwd'][0]
1057
        except KeyError:
1058
            securid_pwd = ''
1059
        securid_register = False
1060
        if request.args.has_key('securid_register'):
1061
            # l'utilisateur a validé l'utilisation de securid
1062
            securid_register = True
1063
        already_registered = False
1064
        if is_true(request.args.get('user_registered', [''])[0]):
1065
            # l'utilisateur a déjà enregistré son identifiant securid
1066
            already_registered = True
1067
        try:
1068
            securid_login = request.args['securid_user'][0]
1069
        except KeyError:
1070
            securid_login = ''
1071
        # vérification du login ticket
1072
        try:
1073
            ticket = request.args['lt'][0]
1074
            assert self.manager.login_sessions.validate_session(ticket)
1075
        except:
1076
            # on n'est pas passé par /login : on renvoie dessus
1077
            if from_url:
1078
                response = RedirectResponse('/login?service=%s' % from_url)
1079
            else:
1080
                response = RedirectResponse('/login')
1081
            return response
1082
        session_infos = self.manager.login_sessions.get_session_info(ticket)
1083
        # stockage des correspondances d'identifiants après retour d'une authentification externe
1084
        # préparation des données (OpenID Connect)
1085
        provider_data = None
1086
        cookies = getCookies(request)
1087
        if 'provider' in session_infos:
1088
            provider_data = session_infos
1089
            # ajout des informations sur le compte local (branche / username)
1090
            provider_data['local_user'] = username
1091
            provider_data['search_branch'] = search_branch
1092
        # Authentification OTP securid
1093
        # XXX implémentation incomplète / mettre à jour la partie gen_request si besoin
1094
        #elif config.SECURID_PROVIDER and securid_register:
1095
        #    # on lance une authentification SecurID distante si demandé
1096
        #    try:
1097
        #        sp_meta = self.manager.get_metadata(config.SECURID_PROVIDER)
1098
        #    except:
1099
        #        log.msg(_('no metadata available for {0}').format(config.SECURID_PROVIDER))
1100
        #        sp_meta = None
1101
        #    if securid_pwd and sp_meta:
1102
        #        # on stocke les informations utiles pour la création de session et le retour
1103
        #        # vers la ressource demandée si nécessaire (après confirmation par le fournisseur
1104
        #        # d'identité gérant l'authentification OTP
1105
        #        session_id = self.manager.securid_sessions.add_session((None, None, None, from_url, redirect_to, username, password, True, cookies))
1106
        #        # recherche du login RSA associé à l'utilisateur local
1107
        #        return self.send_securid_request(session_id, username, password, sp_meta, request)
1108

    
1109
        # on lance une authentification SecurID locale si demandé
1110
        if config.USE_SECURID and securid_register:
1111
            otp_login = self.manager._data_proxy.check_otp_config(search_branch)
1112
            if otp_login == 'identiques':
1113
                registered_user = username
1114
            else:
1115
                registered_user = self.manager.securid_user_store.get_external_user(username, search_branch)
1116
            if already_registered and registered_user:
1117
                securid_login = registered_user
1118
                securid_pwd = password
1119
            css = config.DEFAULT_CSS
1120
            # on regarde si une css est disponible pour ce service
1121
            if from_url:
1122
                css = self.get_css_from_service(from_url)
1123
            # création d'un login ticket avec toutes les informations nécessaires pour continuer la procédure
1124
            login_ticket = self.manager.get_login_ticket((username, search_branch, password, from_url, redirect_to, css, securid_login, securid_pwd, already_registered, cookies))
1125
            # redirection sur la ressource /securid pour validation
1126
            # on passe les paramètres from_url et redirect_to pour continuer
1127
            # la procédure depuis la ressource securid
1128
            req_args = (('lt', login_ticket),)
1129
            response = RedirectResponse('/securid?%s' % urllib.urlencode(req_args))
1130
            return response
1131
        # on va tenter l'authentification sur tous les serveurs d'auth
1132
        defer_auth = self.manager.authenticate(username, password, search_branch)
1133
        return defer_auth.addCallback(self.callb_auth, from_url, redirect_to, cookies, provider_data)
1134

    
1135
    @trace
1136
    def callb_auth(self, res_auth, from_url, redirect_to, request_cookies, provider_data=None):
1137
        """finalise la mise en place de la session après vérification des identifiants et renvoie l'url sur laquelle rediriger (+ cookies à mettre en place)
1138
        res_auth : id de la session créée et données utilisateur (ou None)
1139
        """
1140
        session_id, user_data = res_auth
1141
        if session_id != "":
1142
            if provider_data:
1143
                # stockage des correspondance de compte (local / fournisseur d'identité externe)
1144
                provider = provider_data['provider']
1145
                username = provider_data['username']
1146
                local_user = provider_data['local_user']
1147
                search_branch = provider_data['search_branch']
1148
                # informations de la session locale :
1149
                # enregistrement de la correspondance des comptes
1150
                if provider_data.get('need_register', False):
1151
                    user_store = self.manager.external_providers[provider][1]
1152
                    register_ok = user_store.register_user(local_user, username, search_branch)
1153
                # création d'un app_ticket spécifique pour référencer le fournisseur d'identité (utile pour logout)
1154
                app_ticket = self.manager.get_app_ticket(session_id, from_url, idp_ident=provider_data['provider'])
1155
                self.manager.app_sessions[app_ticket].saml_ident = provider_data['provider']
1156
                self.manager.app_sessions[app_ticket].provider_data = provider_data
1157
            # on stocke un cookie dans le navigateur
1158
            # chaine = '%s,%s'%(session_id), username.encode(config.encoding))
1159
            session_id = session_id.encode(config.encoding)
1160
            cookies = []
1161
            for cookie in request_cookies:
1162
                if cookie.name != 'EoleSSOServer':
1163
                    cookies.append(cookie)
1164
            cookies.append(TwCookie("EoleSSOServer", session_id, path = "/", discard=True, secure=True))
1165
            if from_url == '':
1166
                # pas de service spécifié, on indique que la session est ouverte, ou on redirige si demandé
1167
                if redirect_to != '':
1168
                    target_url = redirect_to
1169
                else:
1170
                    target_url = '/loggedin'
1171
            else:
1172
                # on redirige sur la page de l'application de destination
1173
                app_ticket = self.manager.get_app_ticket(session_id, from_url, from_credentials=True)
1174
                target_url = urljoin(from_url, 'ticket=%s' % app_ticket)
1175
                log.msg(_("Redirecting to calling app with ticket : {0}").format(get_service_from_url(from_url)))
1176
            if self.manager.user_sessions.get(session_id,(None,))[0] == None:
1177
                # Si session délivrée par le parent, rediriger sur lui pour poser un cookie d'auth qu'il puisse lire
1178
                req_args = (('ticket', session_id), ('return_url', target_url))
1179
                response_url = '%s/gen_cookie?%s' % (self.manager.parent_url, urllib.urlencode(req_args))
1180
                log.msg(_("Redirecting to parent server for session setting"))
1181
            else:
1182
                response_url = target_url
1183
            response = RedirectResponse(response_url)
1184
            if cookies:
1185
                response.headers.setHeader('Set-Cookie', cookies)
1186
            return response
1187

    
1188
        # aucun des serveurs n'a authentifié l'utilisateur (meme l'authentification locale), on revient
1189
        # sur la page de saisie de username/password
1190
        if from_url:
1191
            return RedirectResponse('/?service=%s&failed=1' %(from_url))
1192
        return RedirectResponse('/?failed=1')
1193

    
1194
    # XXX cette fonctionnalité n'a pas été retenue
1195
    #def send_securid_request(self, request_id, username, password, sp_meta, req_args):
1196
    #    # print "SEND_SECURID_REQUEST CALLED : ", request_id, username, password, sp_meta, req_args
1197
    #    # demande d'authentification auprès du serveur
1198
    #    # securid_login = self.manager.securid_user_store.get_external_user(username, search_branch)
1199
    #    idp_ident = config.SECURID_PROVIDER
1200
    #    try:
1201
    #        binding, service_url, response_url = saml_utils.get_endpoint(sp_meta, 'SingleSignOnService', ent_type='IDPSSODescriptor')
1202
    #    except InternalError:
1203
    #        # XXX FIXME : CSS / retourner err_msg en fin de procédure ?
1204
    #        traceback.print_exc()
1205
    #        err_msg = _('no usable endpoint for {0}').format(idp_ident)
1206
    #        return http.Response(stream = gen_page(_('Unreachable resource'), err_msg))
1207
    #    # Génération d'une requête d'authentification
1208
    #    # Le login ticket généré précédemment servira d'identifiant de requête
1209
    #    # Il permettra de retrouver les informations d'origine après réponse
1210
    #    # du fournisseur d'identité (in_response_to dans la réponse)
1211
    #    # Envoi de la demande de logout à l'entité correspondante
1212
    #    sign_method, req = saml_message.gen_request(request_id, username, password, config.IDP_IDENTITY, service_url, config.CERTFILE)
1213
    #    # réponse utilisant le binding POST
1214
    #    req = saml_utils.encode_request(req)
1215
    #    form = """<FORM action="%s" method="POST">
1216
    #    <input type="hidden" name="SAMLRequest" value="%s">
1217
    #    <input type="hidden" name="SigAlg" value="%s">
1218
    #    <input type="hidden" name="RelayState" value="%s">
1219
    #    <input type="Submit" value="%s">
1220
    #    """ % (service_url, req, sign_method, 'ADRESSE_DE_RETOUR', _("Proceed to service"))
1221
    #    # XXX stocker arguments nécessaires à la suite de création de session avant redirection (dico {req_id:data})
1222
    #    return http.Response(stream = gen_page(_('Contacting authentication server'), form))
1223

    
1224
    @trace
1225
    def _render(self, request):
1226
        # retour après authentification
1227
        if ('username' in request.args) and (('password' in request.args) or ('securid_pwd' in request.args)):
1228
            return self.render_verify(request)
1229
        else:
1230
            if ('from_provider' in request.args):
1231
                return self.render_provider(request)
1232
            else:
1233
                redirect_to = self.render_content(request)
1234
                return self.return_login(redirect_to)
1235

    
1236
    @trace
1237
    def return_login(self, redirect_to):
1238
        if redirect_to == None:
1239
            return self.set_headers(http.Response(stream=self.content))
1240
        else:
1241
            if redirect_to != '/loggedin' and config.DEBUG_LOG:
1242
                log.msg("Authform, %s : %s" % (_("Redirecting to "), get_service_from_url(redirect_to)))
1243
            return RedirectResponse(redirect_to)
1244

    
1245
    def locateChild(self, request, segments):
1246
        # LOGOUT GENERAL
1247
        if segments[0] == 'logout':
1248
            return saml_resources.SamlLogout(self.manager), ()
1249
        # ERADICATION DES SESSIONS FANTOMES
1250
        user_session = getCookie(request, 'EoleSSOServer')
1251
        if user_session and user_session.value in self.manager.pending_logout:
1252
            log.msg(_("logout interrupted for session {0} : terminating").format(user_session.value.encode(config.encoding)))
1253
            # REDIRECTION SUR LA PAGE DE LOGOUT
1254
            self.manager.logout(user_session.value.encode(config.encoding))
1255
        if segments:
1256
            # URLS CAS
1257
            if segments[0] in ('login','','verify'):
1258
                return self, ()
1259
            elif segments[0] in ['validate','serviceValidate']:
1260
                return CASCompliantResponse(self.manager), ()
1261
            elif segments[0] == 'proxyValidate':
1262
                return CASCompliantResponse(self.manager, True), ()
1263
            elif segments[0] == 'samlValidate':
1264
                return SAMLCompliantResponse(self.manager, True), ()
1265
            elif segments[0] == 'proxy':
1266
                return ProxyResource(self.manager), ()
1267
            elif segments[0] == 'loggedin':
1268
                return LoggedInForm(self.manager), ()
1269
            elif segments[0] == 'gen_cookie':
1270
                return LocalCookie(self.manager), ()
1271
            elif segments[0] == 'unauthorizedService':
1272
                return UnauthorizedService(), ()
1273
            # URLS XMLRPC
1274
            elif segments[0] == 'xmlrpc':
1275
                return self.auth_server, ()
1276
            # URLS DE TEST FCONNECT
1277
            elif segments[0] == 'oidconnect':
1278
                return oidc_resources.OIDConnect(self.manager), ()
1279
            elif segments[0] == 'oidcallback':
1280
                return oidc_resources.OIDCallback(self.manager), ()
1281
            # URLS SAML
1282
            elif segments[0] == 'saml':
1283
                return saml_resources.SamlResponse(self.manager), segments[1:]
1284
            # URL d'IDPDiscovery
1285
            elif segments[0] == 'discovery':
1286
                return saml_resources.IDPDiscoveryService(self.manager), ()
1287
            # URLS POUR VERIFICATION SECURID
1288
            elif config.USE_SECURID and segments[0] == 'securid':
1289
                return SecuridCheck(self.manager), ()
1290
            elif segments[0] == 'check_user_options':
1291
                return UserChecker(self.manager), ()
1292
            # URL DE RECUPERATION DES ETABLISSEMENTS REPLIQUES (DISPATCHER)
1293
            elif segments[0] == 'etabs':
1294
                return InfoEtabs(), ()
1295
            # URLS UTILITAIRES
1296
            elif segments[0] == 'favicon.ico':
1297
                return static.File(self.favicon), ()
1298
            elif segments[0] == 'css':
1299
                return static.File('interface'), segments[1:]
1300
            elif segments[0] == 'scripts':
1301
                return static.File('interface/scripts'), segments[1:]
1302
            elif segments[0] == 'images':
1303
                return static.File('interface/images'), segments[1:]
1304
        return ErrorPage(responsecode.NOT_FOUND, "%s : %s" % (_('Unreachable resource'),  '/'.join(segments))), ()