]> review.fuel-infra Code Review - packages/trusty/python-pysaml2.git/blob - python-pysaml2/example/idp2/idp.py
Add python-pysaml2 2.4.0 for Kilo
[packages/trusty/python-pysaml2.git] / python-pysaml2 / example / idp2 / idp.py
1 #!/usr/bin/env python
2 import argparse
3 import base64
4 import importlib
5 import logging
6 import os
7 import re
8 import socket
9 import time
10
11 from Cookie import SimpleCookie
12 from hashlib import sha1
13 from urlparse import parse_qs
14
15 from saml2 import BINDING_HTTP_ARTIFACT
16 from saml2 import BINDING_URI
17 from saml2 import BINDING_PAOS
18 from saml2 import BINDING_SOAP
19 from saml2 import BINDING_HTTP_REDIRECT
20 from saml2 import BINDING_HTTP_POST
21 from saml2 import server
22 from saml2 import time_util
23 from saml2.authn import is_equal
24
25 from saml2.authn_context import AuthnBroker
26 from saml2.authn_context import PASSWORD
27 from saml2.authn_context import UNSPECIFIED
28 from saml2.authn_context import authn_context_class_ref
29 from saml2.httputil import Response
30 from saml2.httputil import NotFound
31 from saml2.httputil import geturl
32 from saml2.httputil import get_post
33 from saml2.httputil import Redirect
34 from saml2.httputil import Unauthorized
35 from saml2.httputil import BadRequest
36 from saml2.httputil import ServiceError
37 from saml2.ident import Unknown
38 from saml2.metadata import create_metadata_string
39 from saml2.profile import ecp
40 from saml2.s_utils import rndstr
41 from saml2.s_utils import exception_trace
42 from saml2.s_utils import UnknownPrincipal
43 from saml2.s_utils import UnsupportedBinding
44 from saml2.s_utils import PolicyError
45 from saml2.sigver import verify_redirect_signature
46 from saml2.sigver import encrypt_cert_from_item
47
48 from idp_user import USERS
49 from idp_user import EXTRA
50 from mako.lookup import TemplateLookup
51
52 logger = logging.getLogger("saml2.idp")
53 logger.setLevel(logging.WARNING)
54
55
56 class Cache(object):
57     def __init__(self):
58         self.user2uid = {}
59         self.uid2user = {}
60
61
62 def _expiration(timeout, tformat="%a, %d-%b-%Y %H:%M:%S GMT"):
63     """
64
65     :param timeout:
66     :param tformat:
67     :return:
68     """
69     if timeout == "now":
70         return time_util.instant(tformat)
71     elif timeout == "dawn":
72         return time.strftime(tformat, time.gmtime(0))
73     else:
74         # validity time should match lifetime of assertions
75         return time_util.in_a_while(minutes=timeout, format=tformat)
76
77
78 # -----------------------------------------------------------------------------
79
80
81 def dict2list_of_tuples(d):
82     return [(k, v) for k, v in d.items()]
83
84
85 # -----------------------------------------------------------------------------
86
87
88 class Service(object):
89     def __init__(self, environ, start_response, user=None):
90         self.environ = environ
91         logger.debug("ENVIRON: %s" % environ)
92         self.start_response = start_response
93         self.user = user
94
95     def unpack_redirect(self):
96         if "QUERY_STRING" in self.environ:
97             _qs = self.environ["QUERY_STRING"]
98             return dict([(k, v[0]) for k, v in parse_qs(_qs).items()])
99         else:
100             return None
101
102     def unpack_post(self):
103         _dict = parse_qs(get_post(self.environ))
104         logger.debug("unpack_post:: %s" % _dict)
105         try:
106             return dict([(k, v[0]) for k, v in _dict.items()])
107         except Exception:
108             return None
109
110     def unpack_soap(self):
111         try:
112             query = get_post(self.environ)
113             return {"SAMLRequest": query, "RelayState": ""}
114         except Exception:
115             return None
116
117     def unpack_either(self):
118         if self.environ["REQUEST_METHOD"] == "GET":
119             _dict = self.unpack_redirect()
120         elif self.environ["REQUEST_METHOD"] == "POST":
121             _dict = self.unpack_post()
122         else:
123             _dict = None
124         logger.debug("_dict: %s" % _dict)
125         return _dict
126
127     def operation(self, saml_msg, binding):
128         logger.debug("_operation: %s" % saml_msg)
129         if not (saml_msg and 'SAMLRequest' in saml_msg):
130             resp = BadRequest('Error parsing request or no request')
131             return resp(self.environ, self.start_response)
132         else:
133             # saml_msg may also contain Signature and SigAlg
134             if "Signature" in saml_msg:
135                 kwargs = {"signature": saml_msg["signature"],
136                         "sigalg": saml_msg["SigAlg"]}
137             else:
138                 kwargs = {}
139             try:
140                 _encrypt_cert = encrypt_cert_from_item(
141                     saml_msg["req_info"].message)
142                 return self.do(saml_msg["SAMLRequest"], binding,
143                                saml_msg["RelayState"],
144                                encrypt_cert=_encrypt_cert, **kwargs)
145             except KeyError:
146                 # Can live with no relay state
147                 return self.do(saml_msg["SAMLRequest"], binding,
148                                saml_msg["RelayState"], **kwargs)
149
150     def artifact_operation(self, saml_msg):
151         if not saml_msg:
152             resp = BadRequest("Missing query")
153             return resp(self.environ, self.start_response)
154         else:
155             # exchange artifact for request
156             request = IDP.artifact2message(saml_msg["SAMLart"], "spsso")
157             try:
158                 return self.do(request, BINDING_HTTP_ARTIFACT,
159                                saml_msg["RelayState"])
160             except KeyError:
161                 return self.do(request, BINDING_HTTP_ARTIFACT)
162
163     def response(self, binding, http_args):
164         resp = None
165         if binding == BINDING_HTTP_ARTIFACT:
166             resp = Redirect()
167         elif http_args["data"]:
168             resp = Response(http_args["data"], headers=http_args["headers"])
169         else:
170             for header in http_args["headers"]:
171                 if header[0] == "Location":
172                     resp = Redirect(header[1])
173
174         if not resp:
175             resp = ServiceError("Don't know how to return response")
176
177         return resp(self.environ, self.start_response)
178
179     def do(self, query, binding, relay_state="", encrypt_cert=None):
180         pass
181
182     def redirect(self):
183         """ Expects a HTTP-redirect request """
184
185         _dict = self.unpack_redirect()
186         return self.operation(_dict, BINDING_HTTP_REDIRECT)
187
188     def post(self):
189         """ Expects a HTTP-POST request """
190
191         _dict = self.unpack_post()
192         return self.operation(_dict, BINDING_HTTP_POST)
193
194     def artifact(self):
195         # Can be either by HTTP_Redirect or HTTP_POST
196         _dict = self.unpack_either()
197         return self.artifact_operation(_dict)
198
199     def soap(self):
200         """
201         Single log out using HTTP_SOAP binding
202         """
203         logger.debug("- SOAP -")
204         _dict = self.unpack_soap()
205         logger.debug("_dict: %s" % _dict)
206         return self.operation(_dict, BINDING_SOAP)
207
208     def uri(self):
209         _dict = self.unpack_either()
210         return self.operation(_dict, BINDING_SOAP)
211
212     def not_authn(self, key, requested_authn_context):
213         ruri = geturl(self.environ, query=False)
214
215         kwargs = dict(authn_context=requested_authn_context, key=key, redirect_uri=ruri)
216         # Clear cookie, if it already exists
217         kaka = delete_cookie(self.environ, "idpauthn")
218         if kaka:
219             kwargs["headers"] = [kaka]
220         return do_authentication(self.environ, self.start_response, **kwargs)
221
222 # -----------------------------------------------------------------------------
223
224 REPOZE_ID_EQUIVALENT = "uid"
225 FORM_SPEC = """<form name="myform" method="post" action="%s">
226    <input type="hidden" name="SAMLResponse" value="%s" />
227    <input type="hidden" name="RelayState" value="%s" />
228 </form>"""
229
230 # -----------------------------------------------------------------------------
231 # === Single log in ====
232 # -----------------------------------------------------------------------------
233
234
235 class AuthenticationNeeded(Exception):
236     def __init__(self, authn_context=None, *args, **kwargs):
237         Exception.__init__(*args, **kwargs)
238         self.authn_context = authn_context
239
240
241 class SSO(Service):
242     def __init__(self, environ, start_response, user=None):
243         Service.__init__(self, environ, start_response, user)
244         self.binding = ""
245         self.response_bindings = None
246         self.resp_args = {}
247         self.binding_out = None
248         self.destination = None
249         self.req_info = None
250         self.op_type = ""
251
252     def verify_request(self, query, binding):
253         """
254         :param query: The SAML query, transport encoded
255         :param binding: Which binding the query came in over
256         """
257         resp_args = {}
258         if not query:
259             logger.info("Missing QUERY")
260             resp = Unauthorized('Unknown user')
261             return resp_args, resp(self.environ, self.start_response)
262
263         if not self.req_info:
264             self.req_info = IDP.parse_authn_request(query, binding)
265
266         logger.info("parsed OK")
267         _authn_req = self.req_info.message
268         logger.debug("%s" % _authn_req)
269
270         try:
271             self.binding_out, self.destination = IDP.pick_binding(
272                 "assertion_consumer_service",
273                 bindings=self.response_bindings,
274                 entity_id=_authn_req.issuer.text, request=_authn_req)
275         except Exception as err:
276             logger.error("Couldn't find receiver endpoint: %s" % err)
277             raise
278
279         logger.debug("Binding: %s, destination: %s" % (self.binding_out,
280                                                        self.destination))
281
282         resp_args = {}
283         try:
284             resp_args = IDP.response_args(_authn_req)
285             _resp = None
286         except UnknownPrincipal as excp:
287             _resp = IDP.create_error_response(_authn_req.id,
288                                               self.destination, excp)
289         except UnsupportedBinding as excp:
290             _resp = IDP.create_error_response(_authn_req.id,
291                                               self.destination, excp)
292
293         return resp_args, _resp
294
295     def do(self, query, binding_in, relay_state="", encrypt_cert=None):
296         """
297
298         :param query: The request
299         :param binding_in: Which binding was used when receiving the query
300         :param relay_state: The relay state provided by the SP
301         :param encrypt_cert: Cert to use for encryption
302         :return: A response
303         """
304         try:
305             resp_args, _resp = self.verify_request(query, binding_in)
306         except UnknownPrincipal as excp:
307             logger.error("UnknownPrincipal: %s" % (excp,))
308             resp = ServiceError("UnknownPrincipal: %s" % (excp,))
309             return resp(self.environ, self.start_response)
310         except UnsupportedBinding as excp:
311             logger.error("UnsupportedBinding: %s" % (excp,))
312             resp = ServiceError("UnsupportedBinding: %s" % (excp,))
313             return resp(self.environ, self.start_response)
314
315         if not _resp:
316             identity = USERS[self.user].copy()
317             # identity["eduPersonTargetedID"] = get_eptid(IDP, query, session)
318             logger.info("Identity: %s" % (identity,))
319
320             if REPOZE_ID_EQUIVALENT:
321                 identity[REPOZE_ID_EQUIVALENT] = self.user
322             try:
323                 try:
324                     metod = self.environ["idp.authn"]
325                 except KeyError:
326                     pass
327                 else:
328                     resp_args["authn"] = metod
329
330                 _resp = IDP.create_authn_response(
331                     identity, userid=self.user,
332                     encrypt_cert=encrypt_cert,
333                     **resp_args)
334             except Exception as excp:
335                 logging.error(exception_trace(excp))
336                 resp = ServiceError("Exception: %s" % (excp,))
337                 return resp(self.environ, self.start_response)
338
339         logger.info("AuthNResponse: %s" % _resp)
340         if self.op_type == "ecp":
341             kwargs = {"soap_headers": [
342                 ecp.Response(
343                     assertion_consumer_service_url=self.destination)]}
344         else:
345             kwargs = {}
346
347         http_args = IDP.apply_binding(self.binding_out,
348                                       "%s" % _resp, self.destination,
349                                       relay_state, response=True, **kwargs)
350
351         logger.debug("HTTPargs: %s" % http_args)
352         return self.response(self.binding_out, http_args)
353
354     @staticmethod
355     def _store_request(saml_msg):
356         logger.debug("_store_request: %s" % saml_msg)
357         key = sha1(saml_msg["SAMLRequest"]).hexdigest()
358         # store the AuthnRequest
359         IDP.ticket[key] = saml_msg
360         return key
361
362     def redirect(self):
363         """ This is the HTTP-redirect endpoint """
364
365         logger.info("--- In SSO Redirect ---")
366         saml_msg = self.unpack_redirect()
367
368         try:
369             _key = saml_msg["key"]
370             saml_msg = IDP.ticket[_key]
371             self.req_info = saml_msg["req_info"]
372             del IDP.ticket[_key]
373         except KeyError:
374             try:
375                 self.req_info = IDP.parse_authn_request(saml_msg["SAMLRequest"],
376                                                         BINDING_HTTP_REDIRECT)
377             except KeyError:
378                 resp = BadRequest("Message signature verification failure")
379                 return resp(self.environ, self.start_response)
380
381             _req = self.req_info.message
382
383             if "SigAlg" in saml_msg and "Signature" in saml_msg:
384                 # Signed request
385                 issuer = _req.issuer.text
386                 _certs = IDP.metadata.certs(issuer, "any", "signing")
387                 verified_ok = False
388                 for cert in _certs:
389                     if verify_redirect_signature(saml_msg, cert):
390                         verified_ok = True
391                         break
392                 if not verified_ok:
393                     resp = BadRequest("Message signature verification failure")
394                     return resp(self.environ, self.start_response)
395
396             if self.user:
397                 if _req.force_authn is not None and \
398                         _req.force_authn.lower() == 'true':
399                     saml_msg["req_info"] = self.req_info
400                     key = self._store_request(saml_msg)
401                     return self.not_authn(key, _req.requested_authn_context)
402                 else:
403                     return self.operation(saml_msg, BINDING_HTTP_REDIRECT)
404             else:
405                 saml_msg["req_info"] = self.req_info
406                 key = self._store_request(saml_msg)
407                 return self.not_authn(key, _req.requested_authn_context)
408         else:
409             return self.operation(saml_msg, BINDING_HTTP_REDIRECT)
410
411     def post(self):
412         """
413         The HTTP-Post endpoint
414         """
415         logger.info("--- In SSO POST ---")
416         saml_msg = self.unpack_either()
417
418         try:
419             _key = saml_msg["key"]
420             saml_msg = IDP.ticket[_key]
421             self.req_info = saml_msg["req_info"]
422             del IDP.ticket[_key]
423         except KeyError:
424             self.req_info = IDP.parse_authn_request(
425                 saml_msg["SAMLRequest"], BINDING_HTTP_POST)
426             _req = self.req_info.message
427             if self.user:
428                 if _req.force_authn is not None and \
429                         _req.force_authn.lower() == 'true':
430                     saml_msg["req_info"] = self.req_info
431                     key = self._store_request(saml_msg)
432                     return self.not_authn(key, _req.requested_authn_context)
433                 else:
434                     return self.operation(saml_msg, BINDING_HTTP_POST)
435             else:
436                 saml_msg["req_info"] = self.req_info
437                 key = self._store_request(saml_msg)
438                 return self.not_authn(key, _req.requested_authn_context)
439         else:
440             return self.operation(saml_msg, BINDING_HTTP_POST)
441
442     # def artifact(self):
443     # # Can be either by HTTP_Redirect or HTTP_POST
444     #     _req = self._store_request(self.unpack_either())
445     #     if isinstance(_req, basestring):
446     #         return self.not_authn(_req)
447     #     return self.artifact_operation(_req)
448
449     def ecp(self):
450         # The ECP interface
451         logger.info("--- ECP SSO ---")
452         resp = None
453
454         try:
455             authz_info = self.environ["HTTP_AUTHORIZATION"]
456             if authz_info.startswith("Basic "):
457                 try:
458                     _info = base64.b64decode(authz_info[6:])
459                 except TypeError:
460                     resp = Unauthorized()
461                 else:
462                     try:
463                         (user, passwd) = _info.split(":")
464                         if is_equal(PASSWD[user], passwd):
465                             resp = Unauthorized()
466                         self.user = user
467                         self.environ[
468                             "idp.authn"] = AUTHN_BROKER.get_authn_by_accr(
469                             PASSWORD)
470                     except ValueError:
471                         resp = Unauthorized()
472             else:
473                 resp = Unauthorized()
474         except KeyError:
475             resp = Unauthorized()
476
477         if resp:
478             return resp(self.environ, self.start_response)
479
480         _dict = self.unpack_soap()
481         self.response_bindings = [BINDING_PAOS]
482         # Basic auth ?!
483         self.op_type = "ecp"
484         return self.operation(_dict, BINDING_SOAP)
485
486
487 # -----------------------------------------------------------------------------
488 # === Authentication ====
489 # -----------------------------------------------------------------------------
490
491
492 def do_authentication(environ, start_response, authn_context, key,
493                       redirect_uri, headers=None):
494     """
495     Display the login form
496     """
497     logger.debug("Do authentication")
498     auth_info = AUTHN_BROKER.pick(authn_context)
499
500     if len(auth_info):
501         method, reference = auth_info[0]
502         logger.debug("Authn chosen: %s (ref=%s)" % (method, reference))
503         return method(environ, start_response, reference, key, redirect_uri, headers)
504     else:
505         resp = Unauthorized("No usable authentication method")
506         return resp(environ, start_response)
507
508
509 # -----------------------------------------------------------------------------
510
511 PASSWD = {
512     "daev0001": "qwerty",
513     "haho0032": "qwerty",
514     "roland": "dianakra",
515     "babs": "howes",
516     "upper": "crust"}
517
518
519 def username_password_authn(environ, start_response, reference, key,
520                             redirect_uri, headers=None):
521     """
522     Display the login form
523     """
524     logger.info("The login page")
525
526     kwargs = dict(mako_template="login.mako", template_lookup=LOOKUP)
527     if headers:
528         kwargs["headers"] = headers
529
530     resp = Response(**kwargs)
531
532     argv = {
533         "action": "/verify",
534         "login": "",
535         "password": "",
536         "key": key,
537         "authn_reference": reference,
538         "redirect_uri": redirect_uri
539     }
540     logger.info("do_authentication argv: %s" % argv)
541     return resp(environ, start_response, **argv)
542
543
544 def verify_username_and_password(dic):
545     global PASSWD
546     # verify username and password
547     if PASSWD[dic["login"][0]] == dic["password"][0]:
548         return True, dic["login"][0]
549     else:
550         return False, ""
551
552
553 def do_verify(environ, start_response, _):
554     query = parse_qs(get_post(environ))
555
556     logger.debug("do_verify: %s" % query)
557
558     try:
559         _ok, user = verify_username_and_password(query)
560     except KeyError:
561         _ok = False
562         user = None
563
564     if not _ok:
565         resp = Unauthorized("Unknown user or wrong password")
566     else:
567         uid = rndstr(24)
568         IDP.cache.uid2user[uid] = user
569         IDP.cache.user2uid[user] = uid
570         logger.debug("Register %s under '%s'" % (user, uid))
571
572         kaka = set_cookie("idpauthn", "/", uid, query["authn_reference"][0])
573
574         lox = "%s?id=%s&key=%s" % (query["redirect_uri"][0], uid,
575                                    query["key"][0])
576         logger.debug("Redirect => %s" % lox)
577         resp = Redirect(lox, headers=[kaka], content="text/html")
578
579     return resp(environ, start_response)
580
581
582 def not_found(environ, start_response):
583     """Called if no URL matches."""
584     resp = NotFound()
585     return resp(environ, start_response)
586
587
588 # -----------------------------------------------------------------------------
589 # === Single log out ===
590 # -----------------------------------------------------------------------------
591
592 # def _subject_sp_info(req_info):
593 #    # look for the subject
594 #    subject = req_info.subject_id()
595 #    subject = subject.text.strip()
596 #    sp_entity_id = req_info.message.issuer.text.strip()
597 #    return subject, sp_entity_id
598
599 class SLO(Service):
600     def do(self, request, binding, relay_state="", encrypt_cert=None):
601
602         logger.info("--- Single Log Out Service ---")
603         try:
604             logger.debug("req: '%s'" % request)
605             req_info = IDP.parse_logout_request(request, binding)
606         except Exception as exc:
607             logger.error("Bad request: %s" % exc)
608             resp = BadRequest("%s" % exc)
609             return resp(self.environ, self.start_response)
610
611         msg = req_info.message
612         if msg.name_id:
613             lid = IDP.ident.find_local_id(msg.name_id)
614             logger.info("local identifier: %s" % lid)
615             if lid in IDP.cache.user2uid:
616                 uid = IDP.cache.user2uid[lid]
617                 if uid in IDP.cache.uid2user:
618                     del IDP.cache.uid2user[uid]
619                 del IDP.cache.user2uid[lid]
620             # remove the authentication
621             try:
622                 IDP.session_db.remove_authn_statements(msg.name_id)
623             except KeyError as exc:
624                 logger.error("Unknown session: %s" % exc)
625                 resp = ServiceError("Unknown session: %s" % exc)
626                 return resp(self.environ, self.start_response)
627
628         resp = IDP.create_logout_response(msg, [binding])
629
630         if binding == BINDING_SOAP:
631             destination = ""
632             response = False
633         else:
634             binding, destination = IDP.pick_binding("single_logout_service",
635                                                     [binding], "spsso",
636                                                     req_info)
637             response = True
638
639         try:
640             hinfo = IDP.apply_binding(binding, "%s" % resp, destination,
641                                       relay_state, response=response)
642         except Exception as exc:
643             logger.error("ServiceError: %s" % exc)
644             resp = ServiceError("%s" % exc)
645             return resp(self.environ, self.start_response)
646
647         #_tlh = dict2list_of_tuples(hinfo["headers"])
648         delco = delete_cookie(self.environ, "idpauthn")
649         if delco:
650             hinfo["headers"].append(delco)
651         logger.info("Header: %s" % (hinfo["headers"],))
652
653         if binding == BINDING_HTTP_REDIRECT:
654             for key, value in hinfo['headers']:
655                 if key.lower() == 'location':
656                     resp = Redirect(value, headers=hinfo["headers"])
657                     return resp(self.environ, self.start_response)
658
659             resp = ServiceError('missing Location header')
660             return resp(self.environ, self.start_response)
661         else:
662             resp = Response(hinfo["data"], headers=hinfo["headers"])
663             return resp(self.environ, self.start_response)
664
665
666 # ----------------------------------------------------------------------------
667 # Manage Name ID service
668 # ----------------------------------------------------------------------------
669
670
671 class NMI(Service):
672     def do(self, query, binding, relay_state="", encrypt_cert=None):
673         logger.info("--- Manage Name ID Service ---")
674         req = IDP.parse_manage_name_id_request(query, binding)
675         request = req.message
676
677         # Do the necessary stuff
678         name_id = IDP.ident.handle_manage_name_id_request(
679             request.name_id, request.new_id, request.new_encrypted_id,
680             request.terminate)
681
682         logger.debug("New NameID: %s" % name_id)
683
684         _resp = IDP.create_manage_name_id_response(request)
685
686         # It's using SOAP binding
687         hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % _resp, "",
688                                   relay_state, response=True)
689
690         resp = Response(hinfo["data"], headers=hinfo["headers"])
691         return resp(self.environ, self.start_response)
692
693
694 # ----------------------------------------------------------------------------
695 # === Assertion ID request ===
696 # ----------------------------------------------------------------------------
697
698
699 # Only URI binding
700 class AIDR(Service):
701     def do(self, aid, binding, relay_state="", encrypt_cert=None):
702         logger.info("--- Assertion ID Service ---")
703
704         try:
705             assertion = IDP.create_assertion_id_request_response(aid)
706         except Unknown:
707             resp = NotFound(aid)
708             return resp(self.environ, self.start_response)
709
710         hinfo = IDP.apply_binding(BINDING_URI, "%s" % assertion, response=True)
711
712         logger.debug("HINFO: %s" % hinfo)
713         resp = Response(hinfo["data"], headers=hinfo["headers"])
714         return resp(self.environ, self.start_response)
715
716     def operation(self, _dict, binding, **kwargs):
717         logger.debug("_operation: %s" % _dict)
718         if not _dict or "ID" not in _dict:
719             resp = BadRequest('Error parsing request or no request')
720             return resp(self.environ, self.start_response)
721
722         return self.do(_dict["ID"], binding, **kwargs)
723
724
725 # ----------------------------------------------------------------------------
726 # === Artifact resolve service ===
727 # ----------------------------------------------------------------------------
728
729 class ARS(Service):
730     def do(self, request, binding, relay_state="", encrypt_cert=None):
731         _req = IDP.parse_artifact_resolve(request, binding)
732
733         msg = IDP.create_artifact_response(_req, _req.artifact.text)
734
735         hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % msg, "", "",
736                                   response=True)
737
738         resp = Response(hinfo["data"], headers=hinfo["headers"])
739         return resp(self.environ, self.start_response)
740
741
742 # ----------------------------------------------------------------------------
743 # === Authn query service ===
744 # ----------------------------------------------------------------------------
745
746
747 # Only SOAP binding
748 class AQS(Service):
749     def do(self, request, binding, relay_state="", encrypt_cert=None):
750         logger.info("--- Authn Query Service ---")
751         _req = IDP.parse_authn_query(request, binding)
752         _query = _req.message
753
754         msg = IDP.create_authn_query_response(_query.subject,
755                                               _query.requested_authn_context,
756                                               _query.session_index)
757
758         logger.debug("response: %s" % msg)
759         hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % msg, "", "",
760                                   response=True)
761
762         resp = Response(hinfo["data"], headers=hinfo["headers"])
763         return resp(self.environ, self.start_response)
764
765
766 # ----------------------------------------------------------------------------
767 # === Attribute query service ===
768 # ----------------------------------------------------------------------------
769
770
771 # Only SOAP binding
772 class ATTR(Service):
773     def do(self, request, binding, relay_state="", encrypt_cert=None):
774         logger.info("--- Attribute Query Service ---")
775
776         _req = IDP.parse_attribute_query(request, binding)
777         _query = _req.message
778
779         name_id = _query.subject.name_id
780         uid = name_id.text
781         logger.debug("Local uid: %s" % uid)
782         identity = EXTRA[uid]
783
784         # Comes in over SOAP so only need to construct the response
785         args = IDP.response_args(_query, [BINDING_SOAP])
786         msg = IDP.create_attribute_response(identity,
787                                             name_id=name_id, **args)
788
789         logger.debug("response: %s" % msg)
790         hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % msg, "", "",
791                                   response=True)
792
793         resp = Response(hinfo["data"], headers=hinfo["headers"])
794         return resp(self.environ, self.start_response)
795
796
797 # ----------------------------------------------------------------------------
798 # Name ID Mapping service
799 # When an entity that shares an identifier for a principal with an identity
800 # provider wishes to obtain a name identifier for the same principal in a
801 # particular format or federation namespace, it can send a request to
802 # the identity provider using this protocol.
803 # ----------------------------------------------------------------------------
804
805
806 class NIM(Service):
807     def do(self, query, binding, relay_state="", encrypt_cert=None):
808         req = IDP.parse_name_id_mapping_request(query, binding)
809         request = req.message
810         # Do the necessary stuff
811         try:
812             name_id = IDP.ident.handle_name_id_mapping_request(
813                 request.name_id, request.name_id_policy)
814         except Unknown:
815             resp = BadRequest("Unknown entity")
816             return resp(self.environ, self.start_response)
817         except PolicyError:
818             resp = BadRequest("Unknown entity")
819             return resp(self.environ, self.start_response)
820
821         info = IDP.response_args(request)
822         _resp = IDP.create_name_id_mapping_response(name_id, **info)
823
824         # Only SOAP
825         hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % _resp, "", "",
826                                   response=True)
827
828         resp = Response(hinfo["data"], headers=hinfo["headers"])
829         return resp(self.environ, self.start_response)
830
831
832 # ----------------------------------------------------------------------------
833 # Cookie handling
834 # ----------------------------------------------------------------------------
835 def info_from_cookie(kaka):
836     logger.debug("KAKA: %s" % kaka)
837     if kaka:
838         cookie_obj = SimpleCookie(kaka)
839         morsel = cookie_obj.get("idpauthn", None)
840         if morsel:
841             try:
842                 key, ref = base64.b64decode(morsel.value).split(":")
843                 return IDP.cache.uid2user[key], ref
844             except (KeyError, TypeError):
845                 return None, None
846         else:
847             logger.debug("No idpauthn cookie")
848     return None, None
849
850
851 def delete_cookie(environ, name):
852     kaka = environ.get("HTTP_COOKIE", '')
853     logger.debug("delete KAKA: %s" % kaka)
854     if kaka:
855         cookie_obj = SimpleCookie(kaka)
856         morsel = cookie_obj.get(name, None)
857         cookie = SimpleCookie()
858         cookie[name] = ""
859         cookie[name]['path'] = "/"
860         logger.debug("Expire: %s" % morsel)
861         cookie[name]["expires"] = _expiration("dawn")
862         return tuple(cookie.output().split(": ", 1))
863     return None
864
865
866 def set_cookie(name, _, *args):
867     cookie = SimpleCookie()
868     cookie[name] = base64.b64encode(":".join(args))
869     cookie[name]['path'] = "/"
870     cookie[name]["expires"] = _expiration(5)  # 5 minutes from now
871     logger.debug("Cookie expires: %s" % cookie[name]["expires"])
872     return tuple(cookie.output().split(": ", 1))
873
874 # ----------------------------------------------------------------------------
875
876 # map urls to functions
877 AUTHN_URLS = [
878     # sso
879     (r'sso/post$', (SSO, "post")),
880     (r'sso/post/(.*)$', (SSO, "post")),
881     (r'sso/redirect$', (SSO, "redirect")),
882     (r'sso/redirect/(.*)$', (SSO, "redirect")),
883     (r'sso/art$', (SSO, "artifact")),
884     (r'sso/art/(.*)$', (SSO, "artifact")),
885     # slo
886     (r'slo/redirect$', (SLO, "redirect")),
887     (r'slo/redirect/(.*)$', (SLO, "redirect")),
888     (r'slo/post$', (SLO, "post")),
889     (r'slo/post/(.*)$', (SLO, "post")),
890     (r'slo/soap$', (SLO, "soap")),
891     (r'slo/soap/(.*)$', (SLO, "soap")),
892     #
893     (r'airs$', (AIDR, "uri")),
894     (r'ars$', (ARS, "soap")),
895     # mni
896     (r'mni/post$', (NMI, "post")),
897     (r'mni/post/(.*)$', (NMI, "post")),
898     (r'mni/redirect$', (NMI, "redirect")),
899     (r'mni/redirect/(.*)$', (NMI, "redirect")),
900     (r'mni/art$', (NMI, "artifact")),
901     (r'mni/art/(.*)$', (NMI, "artifact")),
902     (r'mni/soap$', (NMI, "soap")),
903     (r'mni/soap/(.*)$', (NMI, "soap")),
904     # nim
905     (r'nim$', (NIM, "soap")),
906     (r'nim/(.*)$', (NIM, "soap")),
907     #
908     (r'aqs$', (AQS, "soap")),
909     (r'attr$', (ATTR, "soap"))
910 ]
911
912 NON_AUTHN_URLS = [
913     #(r'login?(.*)$', do_authentication),
914     (r'verify?(.*)$', do_verify),
915     (r'sso/ecp$', (SSO, "ecp")),
916 ]
917
918 # ----------------------------------------------------------------------------
919
920
921 def metadata(environ, start_response):
922     try:
923         path = args.path
924         if path is None or len(path) == 0:
925             path = os.path.dirname(os.path.abspath(__file__))
926         if path[-1] != "/":
927             path += "/"
928         metadata = create_metadata_string(path + args.config, IDP.config,
929                                           args.valid, args.cert, args.keyfile,
930                                           args.id, args.name, args.sign)
931         start_response('200 OK', [('Content-Type', "text/xml")])
932         return metadata
933     except Exception as ex:
934         logger.error("An error occured while creating metadata:" + ex.message)
935         return not_found(environ, start_response)
936
937
938 def staticfile(environ, start_response):
939     try:
940         path = args.path[:]
941         if path is None or len(path) == 0:
942             path = os.path.dirname(os.path.abspath(__file__))
943         if path[-1] != "/":
944             path += "/"
945         path += environ.get('PATH_INFO', '').lstrip('/')
946         path = os.path.realpath(path)
947         if not path.startswith(args.path):
948             resp = Unauthorized()
949             return resp(environ, start_response)
950         start_response('200 OK', [('Content-Type', "text/xml")])
951         return open(path, 'r').read()
952     except Exception as ex:
953         logger.error("An error occured while creating metadata:" + ex.message)
954         return not_found(environ, start_response)
955
956
957 def application(environ, start_response):
958     """
959     The main WSGI application. Dispatch the current request to
960     the functions from above and store the regular expression
961     captures in the WSGI environment as  `myapp.url_args` so that
962     the functions from above can access the url placeholders.
963
964     If nothing matches, call the `not_found` function.
965     
966     :param environ: The HTTP application environment
967     :param start_response: The application to run when the handling of the 
968         request is done
969     :return: The response as a list of lines
970     """
971
972     path = environ.get('PATH_INFO', '').lstrip('/')
973
974     if path == "metadata":
975         return metadata(environ, start_response)
976
977     kaka = environ.get("HTTP_COOKIE", None)
978     logger.info("<application> PATH: %s" % path)
979
980     if kaka:
981         logger.info("= KAKA =")
982         user, authn_ref = info_from_cookie(kaka)
983         if authn_ref:
984             environ["idp.authn"] = AUTHN_BROKER[authn_ref]
985     else:
986         try:
987             query = parse_qs(environ["QUERY_STRING"])
988             logger.debug("QUERY: %s" % query)
989             user = IDP.cache.uid2user[query["id"][0]]
990         except KeyError:
991             user = None
992
993     url_patterns = AUTHN_URLS
994     if not user:
995         logger.info("-- No USER --")
996         # insert NON_AUTHN_URLS first in case there is no user
997         url_patterns = NON_AUTHN_URLS + url_patterns
998
999     for regex, callback in url_patterns:
1000         match = re.search(regex, path)
1001         if match is not None:
1002             try:
1003                 environ['myapp.url_args'] = match.groups()[0]
1004             except IndexError:
1005                 environ['myapp.url_args'] = path
1006
1007             logger.debug("Callback: %s" % (callback,))
1008             if isinstance(callback, tuple):
1009                 cls = callback[0](environ, start_response, user)
1010                 func = getattr(cls, callback[1])
1011                 return func()
1012             return callback(environ, start_response, user)
1013
1014     if re.search(r'static/.*', path) is not None:
1015         return staticfile(environ, start_response)
1016     return not_found(environ, start_response)
1017
1018 # ----------------------------------------------------------------------------
1019
1020 if __name__ == '__main__':
1021     from wsgiref.simple_server import make_server
1022
1023     parser = argparse.ArgumentParser()
1024     parser.add_argument('-p', dest='path', help='Path to configuration file.')
1025     parser.add_argument('-v', dest='valid',
1026                         help="How long, in days, the metadata is valid from "
1027                              "the time of creation")
1028     parser.add_argument('-c', dest='cert', help='certificate')
1029     parser.add_argument('-i', dest='id',
1030                         help="The ID of the entities descriptor")
1031     parser.add_argument('-k', dest='keyfile',
1032                         help="A file with a key to sign the metadata with")
1033     parser.add_argument('-n', dest='name')
1034     parser.add_argument('-s', dest='sign', action='store_true',
1035                         help="sign the metadata")
1036     parser.add_argument('-m', dest='mako_root', default="./")
1037     parser.add_argument(dest="config")
1038     args = parser.parse_args()
1039
1040     AUTHN_BROKER = AuthnBroker()
1041     AUTHN_BROKER.add(authn_context_class_ref(PASSWORD),
1042                      username_password_authn, 10,
1043                      "http://%s" % socket.gethostname())
1044     AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED),
1045                      "", 0, "http://%s" % socket.gethostname())
1046     CONFIG = importlib.import_module(args.config)
1047     IDP = server.Server(args.config, cache=Cache())
1048     IDP.ticket = {}
1049
1050     _rot = args.mako_root
1051     LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'],
1052                             module_directory=_rot + 'modules',
1053                             input_encoding='utf-8', output_encoding='utf-8')
1054
1055     HOST = CONFIG.HOST
1056     PORT = CONFIG.PORT
1057
1058     SRV = make_server(HOST, PORT, application)
1059     print "IdP listening on %s:%s" % (HOST, PORT)
1060     SRV.serve_forever()