Projet

Général

Profil

Télécharger (20,7 ko) Statistiques
| Branche: | Révision:

root / larpe / trunk / larpe / liberty.ptl @ ebf37dd5

1
import libxml2
2
import urllib
3
import urlparse
4
import httplib
5
import re
6
import os
7

    
8
from quixote import get_field, get_request, get_response, get_session, get_session_manager, redirect
9
from quixote.directory import Directory
10
from quixote.http_request import parse_header
11

    
12
import lasso
13

    
14
import logger
15
import misc
16
import storage
17
from form import *
18
from template import *
19
from users import User
20

    
21

    
22
class Liberty(Directory):
23
    _q_exports = ["", "login", "assertionConsumer", "soapEndpoint",
24
            "singleLogout", "singleLogoutReturn",
25
            "federationTermination", "federationTerminationReturn",
26
            ('metadata.xml', 'metadata'), 'public_key',
27
            'local_auth', 'local_auth_check', 'local_logout']
28

    
29
    def perform_login(self, idp = None):
30
        server = misc.get_lasso_server()
31
        login = lasso.Login(server)
32
        login.initAuthnRequest(idp, lasso.HTTP_METHOD_REDIRECT)
33
        login.request.nameIdPolicy = "federated"
34
        login.request.forceAuthn = False
35
        login.request.isPassive = False
36
        login.request.consent = "urn:liberty:consent:obtained"
37
        login.buildAuthnRequestMsg()
38
        return redirect(login.msgUrl)
39

    
40
    def assertionConsumer(self):
41
        server = misc.get_lasso_server()
42
        if not server:
43
            return error_page(_('Liberty support is not yet configured'))
44
        login = lasso.Login(server)
45
        request = get_request()
46
        if request.get_method() == 'GET' or get_field('LAREQ'):
47
            if request.get_method() == 'GET':
48
                login.initRequest(request.get_query(), lasso.HTTP_METHOD_REDIRECT)
49
            else:
50
                login.initRequest(get_field('LAREQ'), lasso.HTTP_METHOD_POST)
51

    
52
            login.buildRequestMsg()
53
            try:
54
                soap_answer = soap_call(login.msgUrl, login.msgBody)
55
            except SOAPException:
56
                return error_page(_("Failure to communicate with identity provider"))
57
            try:
58
                login.processResponseMsg(soap_answer)
59
            except lasso.Error, error:
60
                if error[0] == lasso.LOGIN_ERROR_STATUS_NOT_SUCCESS:
61
                    return error_page(_('Unknown authentication failure'))
62
                if hasattr(lasso, 'LOGIN_ERROR_UNKNOWN_PRINCIPAL'):
63
                    if error[0] == lasso.LOGIN_ERROR_UNKNOWN_PRINCIPAL:
64
                        return error_page(_('Authentication failure; unknown principal'))
65
                return error_page(_("Identity Provider didn't accept artifact transaction."))
66
        else:
67
            login.processAuthnResponseMsg(get_field('LARES'))
68
        login.acceptSso()
69
        session = get_session()
70
        if login.isSessionDirty:
71
            if login.session:
72
                session.lasso_session_dump = login.session.dump()
73
            else:
74
                session.lasso_session_dump = None
75
        user = self.lookup_user(session, login)
76
        if user:
77
            session.set_user(user.id)
78
        else:
79
            session.set_user('anonymous-%s' % login.nameIdentifier.content)
80
            session.lasso_anonymous_identity_dump = login.identity.dump()
81

    
82
        user = session.get_user()
83
        user_dir = misc.get_abs_path(os.path.join(misc.get_proxied_site_path(), 'users'))
84
        federation_file_name = os.path.join(user_dir, user.name_identifiers[0])
85
        if os.path.isdir(user_dir) and os.path.isfile(federation_file_name):
86
            self.sso_local_login(user.name_identifiers[0])
87
            return redirect('/' + misc.get_proxied_site_name() + '/')
88
        else:
89
            response = get_response()
90
            if session.after_url:
91
                after_url = session.after_url
92
                session.after_url = None
93
                return redirect(after_url)
94
            response.set_status(303)
95
            response.headers['location'] = urlparse.urljoin(request.get_url(), str('local_auth'))
96
            response.content_type = 'text/plain'
97
            return "Your browser should redirect you"
98

    
99
    def lookup_user(self, session, login):
100
        ni = login.nameIdentifier.content
101
        session.name_identifier = ni
102
        nis = list(User.select(lambda x: ni in x.name_identifiers))
103
        if nis:
104
            user = nis[0]
105
        else:
106
            if lasso.WSF_SUPPORT and misc.cfg.get('misc', {}).get('grab-user-with-wsf', False):
107
                disco = lasso.Discovery(login.server)
108
                disco.setSessionFromDump(session.lasso_session_dump)
109
                try:
110
                    disco.initQuery()
111
                except lasso.Error, error:
112
                    pass # XXX: there is no defined error code on lasso side
113
                    service = None
114
                else:
115
                    disco.addRequestedServiceType(lasso.PP_HREF)
116
                    disco.buildRequestMsg()
117
                    soap_answer = soap_call(disco.msgUrl, disco.msgBody)
118
                    disco.processQueryResponseMsg(soap_answer)
119

    
120
                    service = disco.getService()
121

    
122
                if not service:
123
                    return None
124

    
125
                service.initQuery('/pp:PP/pp:InformalName', 'name')
126
                service.addQueryItem('/pp:PP/pp:MsgContact', 'email')
127
                service.buildRequestMsg()
128
                try:
129
                    soap_answer = soap_call(service.msgUrl, service.msgBody)
130
                except SOAPException:
131
                    # it was advertised, it didn't work, too bad.
132
                    return None
133
                service.processQueryResponseMsg(soap_answer)
134

    
135
                email, name = None, None
136

    
137
                emailNode = service.getAnswer('/pp:PP/pp:MsgContact')
138
                if emailNode:
139
                    # horrible <MsgContact>; rebuild email
140
                    doc = libxml2.parseDoc(emailNode)
141
                    node = doc.children.children
142
                    account, provider = None, None
143
                    while node:
144
                        if node.name == 'MsgAccount':
145
                            account = node.getContent()
146
                        if node.name == 'MsgProvider':
147
                            provider = node.getContent()
148
                        node = node.next
149
                    if account and provider:
150
                        email = '%s@%s' % (account, provider)
151
                    else:
152
                        email = ''
153

    
154
                nameNode = service.getAnswer('/pp:PP/pp:InformalName')
155
                if nameNode:
156
                    doc = libxml2.parseDoc(nameNode)
157
                    name = unicode(doc.getContent(), 'utf-8').encode('iso-8859-1')
158
        
159
                if email and name:
160
                    user = User()
161
                    user.email = email
162
                    user.name = name
163
                    user.name_identifiers.append(login.nameIdentifier.content)
164
                    user.lasso_dump = login.identity.dump()
165
                    user.store()
166
                    return user
167

    
168
            return None
169

    
170
        user.lasso_dump = login.identity.dump()
171
        user.store()
172
        return user
173

    
174
    def singleLogout(self):
175
        self.local_logout()
176
        request = get_request()
177
        logout = lasso.Logout(misc.get_lasso_server())
178
        if lasso.isLibertyQuery(request.get_query()):
179
            request = get_request()
180
            try:
181
                logout.processRequestMsg(request.get_query())
182
            except lasso.Error, error:
183
                if error[0] == lasso.DS_ERROR_INVALID_SIGNATURE:
184
                    return error_page(_('Failed to check single logout request signature.'))
185
                raise
186
            return self.slo_idp(logout, get_session())
187
        else:
188
            return self.slo_sp(logout, get_session())
189

    
190
    def local_logout(self):
191
        get_response().expire_cookie('dc_admin', path='/')
192
        get_response().expire_cookie('dc_xd', path='/')
193
        #misc.get_html_page('http://localhost/~heretik/dotclear/ecrire/index.php?logout=1')
194
#        host = 'localhost'
195
#        query = '/~heretik/dotclear/ecrire/index.php?logout=1'
196
#        conn = httplib.HTTPConnection(host)
197
#        conn.request('GET', query, headers = { 'Cookie': 'dc_xd=cbec5e7951856e94d3878c68df6e575a; dc_admin=a%3A3%3A%7Bs%3A7%3A%22user_id%22%3Bs%3A7%3A%22heretik%22%3Bs%3A8%3A%22user_pwd%22%3Bs%3A32%3A%#22cc414bfc9c00475b59c87595299ff31d%22%3Bs%3A8%3A%22remember%22%3Bb%3A0%3B%7D', 'Referer': 'http://localhost/~heretik/dotclear/ecrire/',  })
198
#        response = conn.getresponse()
199
#        conn.close()
200

    
201

    
202
    def singleLogoutReturn(self):
203
        returnUrl = '/' + misc.get_proxied_site_name() + '/'
204
        logout = lasso.Logout(misc.get_lasso_server())
205
        try:
206
            logout.processResponseMsg(get_request().get_query())
207
        except lasso.Error, error:
208
            if error[0] == lasso.PROFILE_ERROR_INVALID_QUERY:
209
                raise AccessError()
210
            if error[0] == lasso.DS_ERROR_INVALID_SIGNATURE:
211
                return error_page(_('Failed to check single logout request signature.'))
212
            if hasattr(lasso, 'LOGOUT_ERROR_REQUEST_DENIED') and \
213
                    error[0] == lasso.LOGOUT_ERROR_REQUEST_DENIED:
214
                return redirect(returnUrl) # ignore silently
215
            elif error[0] == lasso.ERROR_UNDEFINED:
216
                # XXX: unknown status; ignoring for now.
217
                return redirect(returnUrl) # ignore silently
218
            raise
219
        return redirect(returnUrl)
220

    
221
    def slo_idp(self, logout, session):
222
        # Single Logout initiated by IdP
223
        if session.lasso_session_dump:
224
            logout.setSessionFromDump(session.lasso_session_dump)
225
        #user = get_request().user
226
        user = get_session().get_user()
227
        if user and user.lasso_dump:
228
            logout.setIdentityFromDump(user.lasso_dump)
229
        if logout.nameIdentifier.content != session.name_identifier:
230
            raise "no appropriate name identifier in session (%s and %s)" % (
231
                    logout.nameIdentifier.content, session.name_identifier)
232

    
233
        try:
234
            logout.validateRequest()
235
        except lasso.Error, error:
236
            if error[0] != lasso.PROFILE_ERROR_SESSION_NOT_FOUND:
237
                raise
238
        else:
239
            del get_session_manager()[session.id]
240
            get_session_manager().expire_session() 
241

    
242
        logout.buildResponseMsg()
243
        if logout.msgBody: # soap answer
244
            return logout.msgBody
245
        else:
246
            return redirect(logout.msgUrl)
247

    
248
    def slo_sp(self, logout, session):
249
        rootUrl = '/' + misc.get_proxied_site_name() + '/'
250
        if not session.user:
251
            get_session_manager().expire_session()
252
            return redirect(rootUrl)
253

    
254
        if session.lasso_session_dump:
255
            logout.setSessionFromDump(session.lasso_session_dump)
256
        #user = get_request().user
257
        user = get_session().get_user()
258
        if user and user.lasso_dump:
259
            logout.setIdentityFromDump(user.lasso_dump)
260
        return self.slo_sp_redirect(logout)
261

    
262
    def slo_sp_redirect(self, logout):
263
        rootUrl = '/' + misc.get_proxied_site_name() + '/'
264
        try:
265
            logout.initRequest(None, lasso.HTTP_METHOD_REDIRECT)
266
        except lasso.Error, error:
267
            if error[0] == lasso.PROFILE_ERROR_NAME_IDENTIFIER_NOT_FOUND:
268
                get_session_manager().expire_session() 
269
                return redirect(rootUrl)
270
            raise
271
        logout.buildRequestMsg()
272
        get_session_manager().expire_session() 
273
        return redirect(logout.msgUrl)
274

    
275
    def soapEndpoint(self):
276
        request = get_request()
277
        ctype = request.environ.get("CONTENT_TYPE")
278
        if not ctype:
279
            return
280

    
281
        ctype, ctype_params = parse_header(ctype)
282
        if ctype != 'text/xml':
283
            return
284

    
285
        response = get_response()
286
        response.set_content_type('text/xml')
287

    
288
        length = int(request.environ.get('CONTENT_LENGTH'))
289
        soap_message = request.stdin.read(length)
290

    
291
        request_type = lasso.getRequestTypeFromSoapMsg(soap_message) 
292

    
293
        if request_type == lasso.REQUEST_TYPE_LOGOUT:
294
            logout = lasso.Logout(misc.get_lasso_server())
295
            logout.processRequestMsg(soap_message)
296
            name_identifier = logout.nameIdentifier.content
297
            for session in get_session_manager().values():
298
                if name_identifier == session.name_identifier:
299
                    break
300
            else:
301
                raise "session not found"
302
            return self.slo_idp(logout, session)
303

    
304
        if request_type == lasso.REQUEST_TYPE_DEFEDERATION:
305
            defederation = lasso.Defederation(misc.get_lasso_server())
306
            defederation.processNotificationMsg(soap_message)
307
            name_identifier = defederation.nameIdentifier.content
308
            for session in get_session_manager().values():
309
                if name_identifier == session.name_identifier:
310
                    break
311
            else:
312
                # XXX: lookup user, not session!
313
                raise "session not found"
314
            return self.fedterm(defederation, session)
315

    
316
    def defederate(self, idp = None):
317
        session = get_session()
318
        user = session.get_user()
319

    
320
        self.local_defederate(session)
321
        
322
        defederation = lasso.Defederation(misc.get_lasso_server())
323
        defederation.setSessionFromDump(session.lasso_session_dump)
324
        if user and user.lasso_dump:
325
            defederation.setIdentityFromDump(user.lasso_dump)
326

    
327
        defederation.initNotification(idp, lasso.HTTP_METHOD_SOAP);
328
        defederation.buildNotificationMsg();
329

    
330
        try:
331
            soap_call(defederation.msgUrl, defederation.msgBody);
332
        except SOAPException:
333
            return error_page(_("Failure to communicate with identity provider"))
334

    
335
        rootUrl = '/' + misc.get_proxied_site_name() + '/'
336
        return redirect(rootUrl)
337

    
338
    def local_defederate(self, session):
339
        user = session.get_user()
340
        self.local_logout()
341
        if user is not None:
342
            user_dir = misc.get_abs_path(os.path.join(misc.get_proxied_site_path(), 'users'))
343
            federation_file_name = os.path.join(user_dir, user.name_identifiers[0])
344
            if os.path.isfile(federation_file_name):
345
                os.remove(federation_file_name)
346

    
347
#        if hasattr(session, 'auth_cookie'):
348
#            cookies_file_name = misc.get_abs_path(os.path.join(misc.get_proxied_site_path(), 'cookies_to_delete'))
349
#            cookies_file = open(cookies_file_name, 'a')
350
#            cookies_file.write(session.auth_cookie + '\n')
351
#            cookies_file.close()
352
            
353

    
354
    def federationTermination(self):
355
        request = get_request()
356
        if not lasso.isLibertyQuery(request.get_query()):
357
            return redirect('.')
358
        
359
        defederation = lasso.Defederation(misc.get_lasso_server())
360
        defederation.processNotificationMsg(request.get_query())
361
        session = get_session()
362
        return self.fedterm(defederation, session)
363

    
364
    def fedterm(self, defederation, session):
365
        self.local_defederate(session)
366
        defederation.setSessionFromDump(session.lasso_session_dump)
367

    
368
        #user = get_request().user
369
        user = get_session().get_user()
370
        if user and user.lasso_dump:
371
            defederation.setIdentityFromDump(user.lasso_dump)
372

    
373
        try:
374
            defederation.validateNotification()
375
        except lasso.Error, error:
376
            pass # ignore failure (?)
377
        else:
378
            if not defederation.identity:
379
                # if it was the last federation the whole identity dump collapsed
380
                user.lasso_dump = None
381
            else:
382
                user.lasso_dump = defederation.identity.dump()
383
            user.store()
384

    
385
        if defederation.isSessionDirty:
386
            if defederation.session:
387
                session.lasso_session_dump = defederation.session.dump()
388
            else:
389
                session.lasso_session_dump = None
390

    
391
        if defederation.msgUrl:
392
            return redirect(defederation.msgUrl)
393
        else:
394
            get_session_manager().commit_changes(session)
395
            response = get_response()
396
            response.set_status(204)
397
            return ''
398

    
399
    def federationTerminationReturn(self):
400
        rootUrl = '/' + misc.get_proxied_site_name() + '/'
401
        return redirect(rootUrl)
402

    
403
    def metadata(self):
404
        response = get_response()
405
        response.set_content_type('text/xml', 'utf-8')
406
        metadata = unicode(open(misc.get_abs_path(
407
                        misc.cfg['sp']['metadata'])).read(), 'utf-8')
408
        return metadata
409

    
410
    def public_key(self):
411
        response = get_response()
412
        response.set_content_type('application/octet-stream')
413
        publickey = file(misc.get_abs_path(misc.cfg['sp']['publickey'])).read()
414
        return publickey
415

    
416
    def local_auth [html] (self):
417
        response = get_response()
418
        response.set_content_type('text/html')
419
        page = misc.get_html_page(misc.cfg['auth_url'])
420
        old_url = re.compile(str('(form action=")[^"]*"'))
421
        page = old_url.sub(str(r'\1local_auth_check"'), page)
422
        htmltext(page)
423

    
424
    def local_auth_check (self):
425
        request = get_request()
426
        user_id = request.get_field('user_id')
427
        user_password = request.get_field('user_pwd')
428
        status = self.local_auth_check_post(user_id, user_password)
429
        if status == 302:
430
            self.federate(user_id, user_password)
431
            return redirect('/' + get_request().get_path().split('/')[2] + '/')
432
        else:
433
            return self.local_auth()
434

    
435
    def local_auth_check_post (self, user_id, user_password):
436
        url = misc.cfg['auth_url']
437
        if url.startswith('http://'):
438
            host, query = urllib.splithost(url[5:])
439
            conn = httplib.HTTPConnection(host)
440
        else:
441
            host, query = urllib.splithost(url[6:])
442
            conn = httplib.HTTPSConnection(host)
443
        body = 'user_id=%s&user_pwd=%s' % (user_id, user_password)
444
        conn.request("POST", query, body, {'Content-Type': 'application/x-www-form-urlencoded'})
445
        response = conn.getresponse()
446
        conn.close()
447
        cookies = response.getheader('Set-Cookie', None)
448
        cookies_match =  re.findall('dc_admin=([^;]+)', cookies)
449
        if len(cookies_match) > 0:
450
            # Can't use get_response().set_cookie('dc_admin', cookie, path='/') from quixote because
451
            # it adds double quotes
452
            set_cookie = 'dc_admin=%s; path=/' % cookies_match[0]
453
            get_response().set_header('Set-Cookie', set_cookie)
454
#            get_session().auth_cookie = set_cookie
455
        return response.status
456

    
457
    def local_auth_check_form [html] (self):
458
        request = get_request()
459
        user_id = request.get_field('user_id')
460
        user_password = request.get_field('user_pwd')
461
        session = get_session()
462
        session.local_user_id = user_id
463
        session.local_user_password = user_password
464
        """<html><head>
465
        </head>
466
        <body onload="test()">
467
        <script type="text/javascript">
468
        function test() {
469
            document.myform.submit();
470
        }
471
        </script>
472
        <form name="myform" action="%s" method="post">
473
        <input type="text" name="user_id" value="%s" />
474
        <input type="password" name="user_pwd" value="%s" />
475
        </form>
476
        </body></html>""" % (misc.cfg['auth_url'], user_id, user_password)
477

    
478
    def federate(self, user_id, user_password):
479
        liberty_user = get_session().get_user()
480
        if liberty_user is not None:
481
            user_dir = misc.get_abs_path(os.path.join(misc.get_proxied_site_path(), 'users'))
482
            if not os.path.isdir(user_dir):
483
                os.mkdir(user_dir)
484
            federation_file_name = os.path.join(user_dir, liberty_user.name_identifiers[0])
485
            if not os.path.isfile(federation_file_name):
486
                federation_file = open(federation_file_name, 'w')
487
                federation_file.write(user_id + '\n' + user_password)
488
                federation_file.close()
489

    
490
    def sso_local_login(self, name_id):
491
        #proxied_site_dir = get_request().get_path().split('/')[2]
492
        user_dir = misc.get_abs_path(os.path.join(misc.get_proxied_site_path(), 'users'))
493
        federation_file_name = os.path.join(user_dir, name_id)
494
        federation_file = open(federation_file_name, 'r')
495
        data = federation_file.read().split()
496
        user_id = data[0]
497
        user_password = data[1]
498
        federation_file.close()
499
        self.local_auth_check_post(user_id, user_password)
500

    
501

    
502
class SOAPException(Exception):
503
    pass
504

    
505

    
506
def soap_call(url, msg):
507
    if url.startswith('http://'):
508
        host, query = urllib.splithost(url[5:])
509
        conn = httplib.HTTPConnection(host)
510
    else:
511
        host, query = urllib.splithost(url[6:])
512
        conn = httplib.HTTPSConnection(host)
513
    conn.request("POST", query, msg, {'Content-Type': 'text/xml'})
514
    response = conn.getresponse()
515
    data = response.read()
516
    conn.close()
517
    if response.status not in (200, 204): # 204 ok for federation termination
518
        logger.warn('SOAP error (%s) (on %s)' % (response.status, url))
519
        raise SOAPException()
520
    return data
521

    
(7-7/20)