From 922c4008ba8f0de6b39a57ce3dfdff68b6803512 Mon Sep 17 00:00:00 2001
From: Benjamin Dauvergne <bdauvergne@entrouvert.com>
Date: Fri, 16 Dec 2011 22:08:34 +0100
Subject: [PATCH] [idp/saml2] start federation termination handling

---
 authentic2/idp/saml/backend.py         |    5 +
 authentic2/idp/saml/saml2_endpoints.py |  210 +++++++++++++++++++++++++++++++-
 authentic2/idp/saml/urls.py            |    2 +
 3 files changed, 216 insertions(+), 1 deletion(-)

diff --git a/authentic2/idp/saml/backend.py b/authentic2/idp/saml/backend.py
index 2c58b6e..9c64e52 100644
--- a/authentic2/idp/saml/backend.py
+++ b/authentic2/idp/saml/backend.py
@@ -36,6 +36,11 @@ class SamlBackend(object):
                     actions.append(('logout', 'POST',
                         '/idp/%s/idp_slo/' % protocol,
                         (( 'provider_id', entity_id ),)))
+                if models.LibertyFederation.objects.filter(
+                        user=request.user, sp_id=entity_id).exists():
+                    actions.append(('defederate', 'POST',
+                        '/idp/%s/name_id_management/idp/' % protocol,
+                        (( 'provider_id', entity_id),('terminate','1'))))
                 if actions:
                     ls.append(Service(url=None, name=liberty_provider.name,
                         actions=actions))
diff --git a/authentic2/idp/saml/saml2_endpoints.py b/authentic2/idp/saml/saml2_endpoints.py
index fa4169f..9decdfd 100644
--- a/authentic2/idp/saml/saml2_endpoints.py
+++ b/authentic2/idp/saml/saml2_endpoints.py
@@ -92,7 +92,9 @@ metadata_map = (
         (saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE,
             soap_bindings, '/slo/soap'),
         (saml2utils.Saml2Metadata.ARTIFACT_RESOLUTION_SERVICE,
-            lasso.SAML2_METADATA_BINDING_SOAP, '/artifact')
+            lasso.SAML2_METADATA_BINDING_SOAP, '/artifact'),
+        (saml2utils.Saml2Metadata.MANAGE_NAME_ID_SERVICE,
+            soap_bindings+asynchronous_bindings, '/name_id_management'),
 )
 metadata_options = {'key': settings.SAML_SIGNATURE_PUBLIC_KEY}
 
@@ -1641,3 +1643,209 @@ def check_destination(request, req_or_res):
         logger.warning('failure, expected: %r got: %r ' \
             % (destination, req_or_res.destination))
     return result
+
+class NoLogger(object):
+    def error(self, *args, **kwargs):
+        pass
+    def warning(self, *args, **kwargs):
+        pass
+    def info(self, *args, **kwargs):
+        pass
+    def critical(self, *args, **kwargs):
+        pass
+    def debug(self, *args, **kwargs):
+        pass
+
+class RequestProcessing(object):
+    def __init__(self, request, profile_name, request_method, logger):
+        self.profile_name = profile_name
+        self.request_method = request_method
+        self.logger = logger or NoLogger()
+        self.id = 'request-id-unknown'
+
+    def log(self, fn, message, args, kwargs):
+        fn('[%s] %s: %s' % (self.id, self.profile_name, message), *args, **kwargs)
+
+    def info(self, *args, **kwargs):
+        self.log(self.logger.info, args[0], args[1:], kwargs)
+
+    def debug(self, *args, **kwargs):
+        self.log(self.logger.debug, args[0], args[1:], kwargs)
+
+    def error(self, *args, **kwargs):
+        self.log(self.logger.error, args[0], args[1:], kwargs)
+
+    def load_provider(self):
+        remote_provider_id = self.profile.remoteProviderId
+        self.info('loading provider %s', remote_provider_id)
+        self.provider = load_provider(self.request, remote_provider_id, server=self.server)
+        if not self.provider:
+            self.error('unknown provider')
+            return False
+        return True
+
+    def error_response(self, code, requester=True):
+        # build an error response, and return it
+        self.info('returning status code %s', code)
+        return 'not done'
+
+    def set_id(self):
+        if self.profile.request and self.profile.request.id:
+            self.id = '[%s]' % self.profile.request.id
+
+    def call_process_request(self, provider_loaded=False):
+        try:
+            getattr(profile, request_method)(message)
+        except (lasso.ServerProviderNotFoundError, lasso.ProfileUnknownProviderError):
+            self.set_id()
+            if not provider_loaded:
+                if self.load_provider():
+                    return self.call_process_request(self, True)
+            return self.error_response(AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER)
+        except lasso.DsError:
+            self.set_id()
+            self.error('signature error', self.id)
+            return self.error_response(lasso.LIB_STATUS_CODE_INVALID_SIGNATURE)
+        except lasso.Error, e:
+            self.set_id()
+            self.error('lasso error on processing: %s', lasso.strError(e.code))
+            return self.error_response(None)
+        self.set_id()
+        self.info('request processed')
+        return None
+
+    def __call__(self, request):
+        self.request = request
+        self.logger.info('start processing request for %s', self.profile_name)
+        self.server = create_server(self.request)
+        self.profile = getattr(lasso, object_name)(server)
+        if request.method == 'POST':
+            # POST
+            if lasso.SAML2_FIELD_REQUEST in request.POST:
+                message = request.POST[lasso.SAML2_FIELD_REQUEST]
+                self.method = lasso.HTTP_METHOD_POST
+            else: # SOAP
+                message = get_soap_message(request)
+                self.method = lasso.HTTP_METHOD_SOAP
+        elif request.method == 'GET':
+            message = request.META['QUERY_STRING']
+            self.method = lasso.HTTP_METHOD_REDIRECT
+        self.call_process_request()
+
+    def validate(self, request):
+        try:
+            self.profile.validate_request()
+        except lasso.Error, e:
+            self.error('lasso error on validation: %s', lasso.strError(e.code))
+            return self.error_response(None)
+        return None
+
+def name_id_management(request):
+    '''Handle samlp2:NameIDManagementRequest messages'''
+    request_process = RequestProcessing('NameIdManagement', 'process_request_msg')
+    premature_response = request_process(request)
+    if premature_response:
+        return premature_response
+    load_federation(request, request_process.profile)
+    premature_response = request_process.validate(request)
+    if premature_response:
+        return premature_response
+    save_federation(request, request_process.profile)
+    if request_process.profile.request.Terminate:
+        request_process.info('request to terminate federation')
+        # search all federation of current user with provider, and erase them
+        q = LibertyFederation.objects.filter(
+            sp_id=request_process.provider.entity_id)
+        count = q.count()
+        q.delete()
+        if count:
+            request_process.info('deleted %d federation', count)
+        else:
+            request_process.info('no federation found to delete')
+    else:
+        True
+
+def name_id_management_idp_response(request, profile=None, next_url=None,
+        response=None):
+    if not profile:
+        next_url, provider
+
+def name_id_management_idp(request):
+    '''Initiate a samlp2:NameIDManagementRequest, some parameters can be passed
+       to the method:
+         - provider_id: the entity ID of the targetted provider (required)
+         - terminate: if present means that we want to terminate the current
+           federation with the provider.
+         - new_name_id: if terminate is absent, give the name of the new
+           federation to create with the provider.
+         - http_method: an integer giving the http method to use (see Lasso for
+           the integer values) (optional). If absent the first declared
+           http_method in the metadata file is used.
+         - next_url: a return URL, default from settings used if absent
+           (optional)
+    '''
+    if 'SAMLResponse' in request.GET or 'SAMLResponse' in request.POST:
+        return name_id_management_idp_response(request,
+                response=request.POST.get('SAMLResponse') or
+                    request.META['QUERY_STRING'])
+    elif request.method != 'POST':
+        return HttpResponseBadRequest('Only the POST method is supported')
+    try:
+        provider_id = request.POST['provider_id']
+    except KeyError:
+        return HttpResponseBadRequest('Missing provider_id parameter')
+    server = create_server(request)
+    provider = load_provider(request, provider_id, server=server)
+    if not provider:
+        return HttpResponseBadRequest('Unknown provider')
+    http_method = server.getProvider(provider_id) \
+        .getFirstHttpMethod(server, lasso.MD_PROTOCOL_TYPE_MANAGE_NAME_ID)
+    if not http_method:
+        return HttpResponseBadRequest('No HTTP method available')
+    next_url = request.POST.get('next_url', settings.LOGIN_REDIRECT_URL)
+    if 'http_method' in request.POST:
+        try:
+            passed_http_method = int(request.POST['http_method'])
+            if lasso.HTTP_METHOD_NONE < passed_http_method < lasso.HTTP_METHOD_LAST:
+                http_method = passed_http_method
+        except ValueError:
+            pass
+    terminate = 'terminate' in request.POST
+    if not terminate:
+        if 'new_name_id' not in request.POST:
+            return HttpResponseBadRequest('Missing new_name_id parameter')
+        new_name_id = request.POST['new_name_id']
+    else:
+        new_name_id = None
+    profile = lasso.NameIdManagement(server)
+    load_federation(request, profile)
+    try:
+        profile.initRequest(provider_id, new_name_id, http_method)
+    except lasso.Error, e:
+        raise
+    if profile.msgBody and profile.msgUrl: # SOAP
+        try:
+            soap_response = send_soap_request
+        except Exception, e:
+            raise
+        return name_id_management_idp(request, response=soap_response,
+                profile=profile, next_url=next_url)
+    else:
+        save_key_values(profile.request.id, next_url, provider_id)
+        return return_saml2_request(request, provider_id, profile)
+
+
+urlpatterns = patterns('',
+    (r'^metadata$', metadata),
+    (r'^sso$', sso),
+    (r'^continue$', continue_sso),
+    (r'^slo$', slo),
+    (r'^slo/soap$', slo_soap),
+    (r'^idp_slo/(.*)$', idp_slo),
+    (r'^slo_return$', slo_return),
+    (r'^finish_slo$', finish_slo),
+    (r'^artifact$', artifact),
+    (r'^idp_sso/(.*)$', idp_sso),
+    (r'^idp_sso/([^/]*)/([^/]*)$', idp_sso),
+    (r'^idp_sso/([^/]*)/([^/]*)/([^/]*)$', idp_sso),
+)
diff --git a/authentic2/idp/saml/urls.py b/authentic2/idp/saml/urls.py
index bbcba09..71fd7e4 100644
--- a/authentic2/idp/saml/urls.py
+++ b/authentic2/idp/saml/urls.py
@@ -13,4 +13,6 @@ urlpatterns = patterns('authentic2.idp.saml.saml2_endpoints',
     url(r'^idp_sso/(.*)$', 'idp_sso'),
     url(r'^idp_sso/([^/]*)/([^/]*)$', 'idp_sso'),
     url(r'^idp_sso/([^/]*)/([^/]*)/([^/]*)$', 'idp_sso'),
+    url(r'^name_id_management', 'name_id_management'),
+    url(r'^name_id_management/idp', 'name_id_management_idp'),
 )
-- 
1.7.10.4

