pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / tools / saml2metadata.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
4
5 import datetime
6 from ipsilon.tools.certs import Certificate
7 from lxml import etree
8 import lasso
9
10
11 SAML2_NAMEID_MAP = {
12     'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL,
13     'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED,
14     'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY,
15     'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS,
16     'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
17     'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT,
18     'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED,
19     'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS,
20     'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509,
21 }
22
23 SAML2_SERVICE_MAP = {
24     'sso-post': ('SingleSignOnService',
25                  lasso.SAML2_METADATA_BINDING_POST),
26     'sso-redirect': ('SingleSignOnService',
27                      lasso.SAML2_METADATA_BINDING_REDIRECT),
28     'sso-soap': ('SingleSignOnService',
29                  lasso.SAML2_METADATA_BINDING_SOAP),
30     'logout-redirect': ('SingleLogoutService',
31                         lasso.SAML2_METADATA_BINDING_REDIRECT),
32     'slo-soap': ('SingleLogoutService',
33                  lasso.SAML2_METADATA_BINDING_SOAP),
34     'response-post': ('AssertionConsumerService',
35                       lasso.SAML2_METADATA_BINDING_POST),
36     'response-paos': ('AssertionConsumerService',
37                       lasso.SAML2_METADATA_BINDING_PAOS),
38 }
39
40 EDESC = '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF
41 NSMAP = {
42     'md': lasso.SAML2_METADATA_HREF,
43     'ds': lasso.DS_HREF
44 }
45
46 IDPDESC = 'IDPSSODescriptor'
47 SPDESC = 'SPSSODescriptor'
48
49 IDP_ROLE = 'idp'
50 SP_ROLE = 'sp'
51
52
53 # Expire metadata weekly by default
54 MIN_EXP_DEFAULT = 7
55
56
57 def mdElement(_parent, _tag, **kwargs):
58     tag = '{%s}%s' % (lasso.SAML2_METADATA_HREF, _tag)
59     return etree.SubElement(_parent, tag, **kwargs)
60
61
62 def dsElement(_parent, _tag, **kwargs):
63     tag = '{%s}%s' % (lasso.DS_HREF, _tag)
64     return etree.SubElement(_parent, tag, **kwargs)
65
66
67 class Metadata(object):
68
69     def __init__(self, role=None, expiration=None):
70         self.root = etree.Element(EDESC, nsmap=NSMAP)
71         self.entityid = None
72         self.role = None
73         self.set_role(role)
74         self.set_expiration(expiration)
75
76     def set_entity_id(self, url):
77         self.entityid = url
78         self.root.set('entityID', url)
79
80     def set_role(self, role):
81         if role is None:
82             return
83         elif role == IDP_ROLE:
84             description = IDPDESC
85         elif role == SP_ROLE:
86             description = SPDESC
87         else:
88             raise ValueError('invalid role: %s' % role)
89         self.role = mdElement(self.root, description)
90         self.role.set('protocolSupportEnumeration', lasso.SAML2_PROTOCOL_HREF)
91         if role == IDP_ROLE:
92             self.role.set('WantAuthnRequestsSigned', 'true')
93         return self.role
94
95     def set_expiration(self, exp):
96         if exp is None:
97             self.root.set('cacheDuration', "P%dD" % (MIN_EXP_DEFAULT))
98             return
99         elif isinstance(exp, datetime.date):
100             d = datetime.datetime.combine(exp, datetime.date.min.time())
101         elif isinstance(exp, datetime.datetime):
102             d = exp
103         elif isinstance(exp, datetime.timedelta):
104             d = datetime.datetime.utcnow() + exp
105         else:
106             raise TypeError('Invalid expiration date type')
107
108         self.root.set('validUntil', d.isoformat() + 'Z')
109
110     def add_cert(self, certdata, use):
111         desc = mdElement(self.role, 'KeyDescriptor')
112         desc.set('use', use)
113         info = dsElement(desc, 'KeyInfo')
114         data = dsElement(info, 'X509Data')
115         cert = dsElement(data, 'X509Certificate')
116         cert.text = certdata
117
118     def add_certs(self, signcert=None, enccert=None):
119         if signcert:
120             self.add_cert(signcert.get_cert(), 'signing')
121         if enccert:
122             self.add_cert(enccert.get_cert(), 'encryption')
123
124     def add_service(self, service, location, **kwargs):
125         svc = mdElement(self.role, service[0])
126         svc.set('Binding', service[1])
127         svc.set('Location', location)
128         for key, value in kwargs.iteritems():
129             svc.set(key, value)
130
131     def add_allowed_name_format(self, name_format):
132         nameidfmt = mdElement(self.role, 'NameIDFormat')
133         nameidfmt.text = name_format
134
135     def output(self, path=None):
136         data = etree.tostring(self.root, xml_declaration=True,
137                               encoding='UTF-8', pretty_print=True)
138         if path is None:
139             return data
140         else:
141             with open(path, 'w') as f:
142                 f.write(data)
143
144
145 if __name__ == '__main__':
146     import tempfile
147     import shutil
148     import os
149
150     tmpdir = tempfile.mkdtemp()
151
152     try:
153         # Test IDP generation
154         sign_cert = Certificate(tmpdir)
155         sign_cert.generate('idp-signing-cert', 'idp.ipsilon.example.com')
156         enc_cert = Certificate(tmpdir)
157         enc_cert.generate('idp-encryption-cert', 'idp.ipsilon.example.com')
158         idp = Metadata()
159         idp.set_entity_id('https://ipsilon.example.com/idp/metadata')
160         idp.set_role(IDP_ROLE)
161         idp.add_certs(sign_cert, enc_cert)
162         idp.add_service(SAML2_SERVICE_MAP['sso-post'],
163                         'https://ipsilon.example.com/idp/saml2/POST')
164         idp.add_service(SAML2_SERVICE_MAP['sso-redirect'],
165                         'https://ipsilon.example.com/idp/saml2/Redirect')
166         for k in SAML2_NAMEID_MAP:
167             idp.add_allowed_name_format(SAML2_NAMEID_MAP[k])
168         md_file = os.path.join(tmpdir, 'metadata.xml')
169         idp.output(md_file)
170         with open(md_file) as fd:
171             text = fd.read()
172         print '==================== IDP ===================='
173         print text
174         print '============================================='
175
176         # Test SP generation
177         sign_cert = Certificate(tmpdir)
178         sign_cert.generate('sp-signing-cert', 'sp.ipsilon.example.com')
179         sp = Metadata()
180         sp.set_entity_id('https://ipsilon.example.com/samlsp/metadata')
181         sp.set_role(SP_ROLE)
182         sp.add_certs(sign_cert)
183         sp.add_service(SAML2_SERVICE_MAP['logout-redirect'],
184                        'https://ipsilon.example.com/samlsp/logout')
185         sp.add_service(SAML2_SERVICE_MAP['response-post'],
186                        'https://ipsilon.example.com/samlsp/postResponse')
187         md_file = os.path.join(tmpdir, 'metadata.xml')
188         sp.output(md_file)
189         with open(md_file) as fd:
190             text = fd.read()
191         print '===================== SP ===================='
192         print text
193         print '============================================='
194
195     finally:
196         shutil.rmtree(tmpdir)