Allow to set additional custom keys on services
[cascardo/ipsilon.git] / ipsilon / tools / saml2metadata.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 from ipsilon.tools.certs import Certificate
21 from lxml import etree
22 import lasso
23
24
25 SAML2_NAMEID_MAP = {
26     'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL,
27     'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED,
28     'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY,
29     'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS,
30     'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
31     'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT,
32     'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,
33     'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS,
34     'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509,
35 }
36
37 SAML2_SERVICE_MAP = {
38     'sso-post': ('SingleSignOnService',
39                  lasso.SAML2_METADATA_BINDING_POST),
40     'sso-redirect': ('SingleSignOnService',
41                      lasso.SAML2_METADATA_BINDING_REDIRECT),
42     'logout-redirect': ('SingleLogoutService',
43                         lasso.SAML2_METADATA_BINDING_REDIRECT),
44     'response-post': ('AssertionConsumerService',
45                       lasso.SAML2_METADATA_BINDING_POST)
46 }
47
48 EDESC = '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF
49 NSMAP = {
50     'md': lasso.SAML2_METADATA_HREF,
51     'ds': lasso.DS_HREF
52 }
53
54 IDPDESC = 'IDPSSODescriptor'
55 SPDESC = 'SPSSODescriptor'
56
57 IDP_ROLE = 'idp'
58 SP_ROLE = 'sp'
59
60
61 def mdElement(_parent, _tag, **kwargs):
62     tag = '{%s}%s' % (lasso.SAML2_METADATA_HREF, _tag)
63     return etree.SubElement(_parent, tag, **kwargs)
64
65
66 def dsElement(_parent, _tag, **kwargs):
67     tag = '{%s}%s' % (lasso.DS_HREF, _tag)
68     return etree.SubElement(_parent, tag, **kwargs)
69
70
71 class Metadata(object):
72
73     def __init__(self, role=None):
74         self.root = etree.Element(EDESC, nsmap=NSMAP)
75         self.entityid = None
76         self.role = None
77         self.set_role(role)
78
79     def set_entity_id(self, url):
80         self.entityid = url
81         self.root.set('entityID', url)
82
83     def set_role(self, role):
84         if role is None:
85             return
86         elif role == IDP_ROLE:
87             description = IDPDESC
88         elif role == SP_ROLE:
89             description = SPDESC
90         else:
91             raise ValueError('invalid role: %s' % role)
92         self.role = mdElement(self.root, description)
93         self.role.set('protocolSupportEnumeration', lasso.SAML2_PROTOCOL_HREF)
94         return self.role
95
96     def add_cert(self, certdata, use):
97         desc = mdElement(self.role, 'KeyDescriptor')
98         desc.set('use', use)
99         info = dsElement(desc, 'KeyInfo')
100         data = dsElement(info, 'X509Data')
101         cert = dsElement(data, 'X509Certificate')
102         cert.text = certdata
103
104     def add_certs(self, signcert=None, enccert=None):
105         if signcert:
106             self.add_cert(signcert.get_cert(), 'signing')
107         if enccert:
108             self.add_cert(enccert.get_cert(), 'encryption')
109
110     def add_service(self, service, location, **kwargs):
111         svc = mdElement(self.role, service[0])
112         svc.set('Binding', service[1])
113         svc.set('Location', location)
114         for key, value in kwargs.iteritems():
115             svc.set(key, value)
116
117     def add_allowed_name_format(self, name_format):
118         nameidfmt = mdElement(self.role, 'NameIDFormat')
119         nameidfmt.text = name_format
120
121     def output(self, path):
122         data = etree.tostring(self.root, xml_declaration=True,
123                               encoding='UTF-8', pretty_print=True)
124         with open(path, 'w') as f:
125             f.write(data)
126
127
128 if __name__ == '__main__':
129     import tempfile
130     import shutil
131     import os
132
133     tmpdir = tempfile.mkdtemp()
134
135     try:
136         # Test IDP generation
137         sign_cert = Certificate(tmpdir)
138         sign_cert.generate('idp-signing-cert', 'idp.ipsilon.example.com')
139         enc_cert = Certificate(tmpdir)
140         enc_cert.generate('idp-encryption-cert', 'idp.ipsilon.example.com')
141         idp = Metadata()
142         idp.set_entity_id('https://ipsilon.example.com/idp/metadata')
143         idp.set_role(IDP_ROLE)
144         idp.add_certs(sign_cert, enc_cert)
145         idp.add_service(SAML2_SERVICE_MAP['sso-post'],
146                         'https://ipsilon.example.com/idp/saml2/POST')
147         idp.add_service(SAML2_SERVICE_MAP['sso-redirect'],
148                         'https://ipsilon.example.com/idp/saml2/Redirect')
149         for k in SAML2_NAMEID_MAP:
150             idp.add_allowed_name_format(SAML2_NAMEID_MAP[k])
151         md_file = os.path.join(tmpdir, 'metadata.xml')
152         idp.output(md_file)
153         with open(md_file) as fd:
154             text = fd.read()
155         print '==================== IDP ===================='
156         print text
157         print '============================================='
158
159         # Test SP generation
160         sign_cert = Certificate(tmpdir)
161         sign_cert.generate('sp-signing-cert', 'sp.ipsilon.example.com')
162         sp = Metadata()
163         sp.set_entity_id('https://ipsilon.example.com/samlsp/metadata')
164         sp.set_role(SP_ROLE)
165         sp.add_certs(sign_cert)
166         sp.add_service(SAML2_SERVICE_MAP['logout-redirect'],
167                        'https://ipsilon.example.com/samlsp/logout')
168         sp.add_service(SAML2_SERVICE_MAP['response-post'],
169                        'https://ipsilon.example.com/samlsp/postResponse')
170         md_file = os.path.join(tmpdir, 'metadata.xml')
171         sp.output(md_file)
172         with open(md_file) as fd:
173             text = fd.read()
174         print '===================== SP ===================='
175         print text
176         print '============================================='
177
178     finally:
179         shutil.rmtree(tmpdir)