Fix generation fo server's metadata file
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.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.providers.common import ProviderBase, ProviderPageBase
21 from ipsilon.providers.common import FACILITY
22 from ipsilon.providers.saml2.auth import AuthenticateRequest
23 from ipsilon.providers.saml2.admin import AdminPage
24 from ipsilon.providers.saml2.provider import IdentityProvider
25 from ipsilon.tools.certs import Certificate
26 from ipsilon.tools import saml2metadata as metadata
27 from ipsilon.tools import files
28 from ipsilon.util.user import UserSession
29 from ipsilon.util.plugin import PluginObject
30 import cherrypy
31 import lasso
32 import os
33
34
35 class Redirect(AuthenticateRequest):
36
37     def GET(self, *args, **kwargs):
38
39         query = cherrypy.request.query_string
40
41         login = self.saml2login(query)
42         return self.auth(login)
43
44
45 class POSTAuth(AuthenticateRequest):
46
47     def POST(self, *args, **kwargs):
48
49         request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
50         relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
51
52         login = self.saml2login(request)
53         login.set_msgRelayState(relaystate)
54         return self.auth(login)
55
56
57 class Continue(AuthenticateRequest):
58
59     def GET(self, *args, **kwargs):
60
61         session = UserSession()
62         user = session.get_user()
63         session.nuke_data('login', 'Return')
64         self.stage = session.get_data('saml2', 'stage')
65
66         if user.is_anonymous:
67             self._debug("User is marked anonymous?!")
68             # TODO: Return to SP with auth failed error
69             raise cherrypy.HTTPError(401)
70
71         self._debug('Continue auth for %s' % user.name)
72
73         dump = session.get_data('saml2', 'Request')
74         if not dump:
75             self._debug("Couldn't find Request dump?!")
76             # TODO: Return to SP with auth failed error
77             raise cherrypy.HTTPError(400)
78
79         try:
80             login = self.cfg.idp.get_login_handler(dump)
81         except Exception, e:  # pylint: disable=broad-except
82             self._debug('Failed to load status from dump: %r' % e)
83
84         if not login:
85             self._debug("Empty Request dump?!")
86             # TODO: Return to SP with auth failed error
87             raise cherrypy.HTTPError(400)
88
89         return self.auth(login)
90
91
92 class SSO(ProviderPageBase):
93
94     def __init__(self, *args, **kwargs):
95         super(SSO, self).__init__(*args, **kwargs)
96         self.Redirect = Redirect(*args, **kwargs)
97         self.POST = POSTAuth(*args, **kwargs)
98         self.Continue = Continue(*args, **kwargs)
99
100
101 class Metadata(ProviderPageBase):
102     def GET(self, *args, **kwargs):
103         with open(self.cfg.idp_metadata_file) as m:
104             body = m.read()
105         cherrypy.response.headers["Content-Type"] = "text/xml"
106         cherrypy.response.headers["Content-Disposition"] = \
107             'attachment; filename="metadata.xml"'
108         return body
109
110
111 class SAML2(ProviderPageBase):
112
113     def __init__(self, *args, **kwargs):
114         super(SAML2, self).__init__(*args, **kwargs)
115         self.metadata = Metadata(*args, **kwargs)
116
117         # Init IDP data
118         try:
119             self.cfg.idp = IdentityProvider(self.cfg)
120         except Exception, e:  # pylint: disable=broad-except
121             self._debug('Failed to init SAML2 provider: %r' % e)
122             return
123
124         # Import all known applications
125         data = self.cfg.get_data()
126         for idval in data:
127             sp = data[idval]
128             if 'type' not in sp or sp['type'] != 'SP':
129                 continue
130             if 'name' not in sp or 'metadata' not in sp:
131                 continue
132             try:
133                 self.cfg.idp.add_provider(sp)
134             except Exception, e:  # pylint: disable=broad-except
135                 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
136
137         self.SSO = SSO(*args, **kwargs)
138
139
140 class IdpProvider(ProviderBase):
141
142     def __init__(self):
143         super(IdpProvider, self).__init__('saml2', 'saml2')
144         self.page = None
145         self.idp = None
146         self.description = """
147 Provides SAML 2.0 authentication infrastructure. """
148
149         self._options = {
150             'idp storage path': [
151                 """ Path to data storage accessible by the IdP """,
152                 'string',
153                 '/var/lib/ipsilon/saml2'
154             ],
155             'idp metadata file': [
156                 """ The IdP Metadata file genearated at install time. """,
157                 'string',
158                 'metadata.xml'
159             ],
160             'idp certificate file': [
161                 """ The IdP PEM Certificate genearated at install time. """,
162                 'string',
163                 'certificate.pem'
164             ],
165             'idp key file': [
166                 """ The IdP Certificate Key genearated at install time. """,
167                 'string',
168                 'certificate.key'
169             ],
170             'allow self registration': [
171                 """ Allow authenticated users to register applications. """,
172                 'boolean',
173                 True
174             ],
175             'default allowed nameids': [
176                 """Default Allowed NameIDs for Service Providers. """,
177                 'list',
178                 ['persistent', 'transient', 'email', 'kerberos', 'x509']
179             ],
180             'default nameid': [
181                 """Default NameID used by Service Providers. """,
182                 'string',
183                 'persistent'
184             ],
185             'default email domain': [
186                 """Default email domain, for users missing email property.""",
187                 'string',
188                 'example.com'
189             ]
190         }
191         if cherrypy.config.get('debug', False):
192             import logging
193             import sys
194             logger = logging.getLogger('lasso')
195             lh = logging.StreamHandler(sys.stderr)
196             logger.addHandler(lh)
197             logger.setLevel(logging.DEBUG)
198
199     @property
200     def allow_self_registration(self):
201         return self.get_config_value('allow self registration')
202
203     @property
204     def idp_storage_path(self):
205         return self.get_config_value('idp storage path')
206
207     @property
208     def idp_metadata_file(self):
209         return os.path.join(self.idp_storage_path,
210                             self.get_config_value('idp metadata file'))
211
212     @property
213     def idp_certificate_file(self):
214         return os.path.join(self.idp_storage_path,
215                             self.get_config_value('idp certificate file'))
216
217     @property
218     def idp_key_file(self):
219         return os.path.join(self.idp_storage_path,
220                             self.get_config_value('idp key file'))
221
222     @property
223     def default_allowed_nameids(self):
224         return self.get_config_value('default allowed nameids')
225
226     @property
227     def default_nameid(self):
228         return self.get_config_value('default nameid')
229
230     @property
231     def default_email_domain(self):
232         return self.get_config_value('default email domain')
233
234     def get_tree(self, site):
235         self.page = SAML2(site, self)
236         self.admin = AdminPage(site, self)
237         return self.page
238
239
240 class Installer(object):
241
242     def __init__(self):
243         self.name = 'saml2'
244         self.ptype = 'provider'
245
246     def install_args(self, group):
247         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
248                            help='Configure SAML2 Provider')
249
250     def configure(self, opts):
251         if opts['saml2'] != 'yes':
252             return
253
254         # Check storage path is present or create it
255         path = os.path.join(opts['data_dir'], 'saml2')
256         if not os.path.exists(path):
257             os.makedirs(path, 0700)
258
259         # Use the same cert for signing and ecnryption for now
260         cert = Certificate(path)
261         cert.generate('idp', opts['hostname'])
262
263         # Generate Idp Metadata
264         url = 'https://' + opts['hostname'] + '/' + opts['instance'] + '/saml2'
265         meta = metadata.Metadata(metadata.IDP_ROLE)
266         meta.set_entity_id(url + '/metadata')
267         meta.add_certs(cert, cert)
268         meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
269                          url + '/SSO/POST')
270         meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
271                          url + '/SSO/Redirect')
272
273         meta.add_allowed_name_format(
274             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
275         meta.add_allowed_name_format(
276             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
277         meta.add_allowed_name_format(
278             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
279         if 'krb' in opts and opts['krb'] == 'yes':
280             meta.add_allowed_name_format(
281                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
282
283         meta.output(os.path.join(path, 'metadata.xml'))
284
285         # Add configuration data to database
286         po = PluginObject()
287         po.name = 'saml2'
288         po.wipe_data()
289
290         po.wipe_config_values(FACILITY)
291         config = {'idp storage path': path,
292                   'idp metadata file': 'metadata.xml',
293                   'idp certificate file': cert.cert,
294                   'idp key file': cert.key,
295                   'enabled': '1'}
296         po.set_config(config)
297         po.save_plugin_config(FACILITY)
298
299         # Fixup permissions so only the ipsilon user can read these files
300         files.fix_user_dirs(path, opts['system_user'])