Add support for logout over SOAP
[cascardo/ipsilon.git] / ipsilon / tools / saml2metadata.py
index fc2e02c..98e7c67 100755 (executable)
@@ -1,22 +1,8 @@
 #!/usr/bin/python
 #
-# Copyright (C) 2014  Simo Sorce <simo@redhat.com>
-#
-# see file 'COPYING' for use and warranty information
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
 
+import datetime
 from ipsilon.tools.certs import Certificate
 from lxml import etree
 import lasso
@@ -34,6 +20,20 @@ SAML2_NAMEID_MAP = {
     'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509,
 }
 
+SAML2_SERVICE_MAP = {
+    'sso-post': ('SingleSignOnService',
+                 lasso.SAML2_METADATA_BINDING_POST),
+    'sso-redirect': ('SingleSignOnService',
+                     lasso.SAML2_METADATA_BINDING_REDIRECT),
+    'sso-soap': ('SingleSignOnService',
+                 lasso.SAML2_METADATA_BINDING_SOAP),
+    'logout-redirect': ('SingleLogoutService',
+                        lasso.SAML2_METADATA_BINDING_REDIRECT),
+    'slo-soap': ('SingleLogoutService',
+                 lasso.SAML2_METADATA_BINDING_SOAP),
+    'response-post': ('AssertionConsumerService',
+                      lasso.SAML2_METADATA_BINDING_POST)
+}
 
 EDESC = '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF
 NSMAP = {
@@ -47,9 +47,9 @@ SPDESC = 'SPSSODescriptor'
 IDP_ROLE = 'idp'
 SP_ROLE = 'sp'
 
-SSO_SERVICE = 'SingleSignOnService'
-LOGOUT_SERVICE = 'SingleLogoutService'
-ASSERTION_SERVICE = 'AssertionConsumerService'
+
+# Expire metadata weekly by default
+MIN_EXP_DEFAULT = 7
 
 
 def mdElement(_parent, _tag, **kwargs):
@@ -64,11 +64,12 @@ def dsElement(_parent, _tag, **kwargs):
 
 class Metadata(object):
 
-    def __init__(self, role=None):
+    def __init__(self, role=None, expiration=None):
         self.root = etree.Element(EDESC, nsmap=NSMAP)
         self.entityid = None
         self.role = None
         self.set_role(role)
+        self.set_expiration(expiration)
 
     def set_entity_id(self, url):
         self.entityid = url
@@ -87,6 +88,21 @@ class Metadata(object):
         self.role.set('protocolSupportEnumeration', lasso.SAML2_PROTOCOL_HREF)
         return self.role
 
+    def set_expiration(self, exp):
+        if exp is None:
+            self.root.set('cacheDuration', "P%dD" % (MIN_EXP_DEFAULT))
+            return
+        elif isinstance(exp, datetime.date):
+            d = datetime.datetime.combine(exp, datetime.date.min.time())
+        elif isinstance(exp, datetime.datetime):
+            d = exp
+        elif isinstance(exp, datetime.timedelta):
+            d = datetime.datetime.now() + exp
+        else:
+            raise TypeError('Invalid expiration date type')
+
+        self.root.set('validUntil', d.isoformat())
+
     def add_cert(self, certdata, use):
         desc = mdElement(self.role, 'KeyDescriptor')
         desc.set('use', use)
@@ -101,20 +117,25 @@ class Metadata(object):
         if enccert:
             self.add_cert(enccert.get_cert(), 'encryption')
 
-    def add_service(self, svctype, binding, location):
-        svc = mdElement(self.role, svctype)
-        svc.set('Binding', binding)
+    def add_service(self, service, location, **kwargs):
+        svc = mdElement(self.role, service[0])
+        svc.set('Binding', service[1])
         svc.set('Location', location)
+        for key, value in kwargs.iteritems():
+            svc.set(key, value)
 
     def add_allowed_name_format(self, name_format):
         nameidfmt = mdElement(self.role, 'NameIDFormat')
         nameidfmt.text = name_format
 
-    def output(self, path):
+    def output(self, path=None):
         data = etree.tostring(self.root, xml_declaration=True,
                               encoding='UTF-8', pretty_print=True)
-        with open(path, 'w') as f:
-            f.write(data)
+        if path is None:
+            return data
+        else:
+            with open(path, 'w') as f:
+                f.write(data)
 
 
 if __name__ == '__main__':
@@ -134,9 +155,9 @@ if __name__ == '__main__':
         idp.set_entity_id('https://ipsilon.example.com/idp/metadata')
         idp.set_role(IDP_ROLE)
         idp.add_certs(sign_cert, enc_cert)
-        idp.add_service(SSO_SERVICE, lasso.SAML2_METADATA_BINDING_POST,
+        idp.add_service(SAML2_SERVICE_MAP['sso-post'],
                         'https://ipsilon.example.com/idp/saml2/POST')
-        idp.add_service(SSO_SERVICE, lasso.SAML2_METADATA_BINDING_REDIRECT,
+        idp.add_service(SAML2_SERVICE_MAP['sso-redirect'],
                         'https://ipsilon.example.com/idp/saml2/Redirect')
         for k in SAML2_NAMEID_MAP:
             idp.add_allowed_name_format(SAML2_NAMEID_MAP[k])
@@ -155,9 +176,9 @@ if __name__ == '__main__':
         sp.set_entity_id('https://ipsilon.example.com/samlsp/metadata')
         sp.set_role(SP_ROLE)
         sp.add_certs(sign_cert)
-        sp.add_service(LOGOUT_SERVICE, lasso.SAML2_METADATA_BINDING_REDIRECT,
+        sp.add_service(SAML2_SERVICE_MAP['logout-redirect'],
                        'https://ipsilon.example.com/samlsp/logout')
-        sp.add_service(ASSERTION_SERVICE, lasso.SAML2_METADATA_BINDING_POST,
+        sp.add_service(SAML2_SERVICE_MAP['response-post'],
                        'https://ipsilon.example.com/samlsp/postResponse')
         md_file = os.path.join(tmpdir, 'metadata.xml')
         sp.output(md_file)