c8f5dab9af21dcb70360d02b5fc435e920b73d1e
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
1 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from ipsilon.providers.common import ProviderBase, ProviderPageBase
19 from ipsilon.providers.saml2.auth import AuthenticateRequest
20 from ipsilon.providers.saml2.admin import Saml2AdminPage
21 from ipsilon.providers.saml2.provider import IdentityProvider
22 from ipsilon.tools.certs import Certificate
23 from ipsilon.tools import saml2metadata as metadata
24 from ipsilon.tools import files
25 from ipsilon.util.user import UserSession
26 from ipsilon.util.plugin import PluginObject
27 from ipsilon.util import config as pconfig
28 import cherrypy
29 from datetime import timedelta
30 import lasso
31 import os
32 import time
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         transdata = self.trans.retrieve()
64         self.stage = transdata['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         if 'saml2_request' not in transdata:
74             self._debug("Couldn't find Request dump?!")
75             # TODO: Return to SP with auth failed error
76             raise cherrypy.HTTPError(400)
77         dump = transdata['saml2_request']
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 # one week
102 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
103 # 30 days
104 METADATA_VALIDITY_PERIOD = 30
105
106
107 class Metadata(ProviderPageBase):
108     def GET(self, *args, **kwargs):
109
110         body = self._get_metadata()
111         cherrypy.response.headers["Content-Type"] = "text/xml"
112         cherrypy.response.headers["Content-Disposition"] = \
113             'attachment; filename="metadata.xml"'
114         return body
115
116     def _get_metadata(self):
117         if os.path.isfile(self.cfg.idp_metadata_file):
118             s = os.stat(self.cfg.idp_metadata_file)
119             if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
120                 with open(self.cfg.idp_metadata_file) as m:
121                     return m.read()
122
123         # Otherwise generate and save
124         idp_cert = Certificate()
125         idp_cert.import_cert(self.cfg.idp_certificate_file,
126                              self.cfg.idp_key_file)
127         meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
128                                     timedelta(METADATA_VALIDITY_PERIOD))
129         body = meta.output()
130         with open(self.cfg.idp_metadata_file, 'w+') as m:
131             m.write(body)
132         return body
133
134
135 class SAML2(ProviderPageBase):
136
137     def __init__(self, *args, **kwargs):
138         super(SAML2, self).__init__(*args, **kwargs)
139         self.metadata = Metadata(*args, **kwargs)
140         self.SSO = SSO(*args, **kwargs)
141
142
143 class IdpProvider(ProviderBase):
144
145     def __init__(self, *pargs):
146         super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
147         self.admin = None
148         self.page = None
149         self.idp = None
150         self.description = """
151 Provides SAML 2.0 authentication infrastructure. """
152
153         self.new_config(
154             self.name,
155             pconfig.String(
156                 'idp storage path',
157                 'Path to data storage accessible by the IdP.',
158                 '/var/lib/ipsilon/saml2'),
159             pconfig.String(
160                 'idp metadata file',
161                 'The IdP Metadata file genearated at install time.',
162                 'metadata.xml'),
163             pconfig.String(
164                 'idp certificate file',
165                 'The IdP PEM Certificate genearated at install time.',
166                 'certificate.pem'),
167             pconfig.String(
168                 'idp key file',
169                 'The IdP Certificate Key genearated at install time.',
170                 'certificate.key'),
171             pconfig.Condition(
172                 'allow self registration',
173                 'Allow authenticated users to register applications.',
174                 True),
175             pconfig.Choice(
176                 'default allowed nameids',
177                 'Default Allowed NameIDs for Service Providers.',
178                 metadata.SAML2_NAMEID_MAP.keys(),
179                 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
180             pconfig.Pick(
181                 'default nameid',
182                 'Default NameID used by Service Providers.',
183                 metadata.SAML2_NAMEID_MAP.keys(),
184                 'persistent'),
185             pconfig.String(
186                 'default email domain',
187                 'Used for users missing the email property.',
188                 'example.com'),
189         )
190         if cherrypy.config.get('debug', False):
191             import logging
192             import sys
193             logger = logging.getLogger('lasso')
194             lh = logging.StreamHandler(sys.stderr)
195             logger.addHandler(lh)
196             logger.setLevel(logging.DEBUG)
197
198     @property
199     def allow_self_registration(self):
200         return self.get_config_value('allow self registration')
201
202     @property
203     def idp_storage_path(self):
204         return self.get_config_value('idp storage path')
205
206     @property
207     def idp_metadata_file(self):
208         return os.path.join(self.idp_storage_path,
209                             self.get_config_value('idp metadata file'))
210
211     @property
212     def idp_certificate_file(self):
213         return os.path.join(self.idp_storage_path,
214                             self.get_config_value('idp certificate file'))
215
216     @property
217     def idp_key_file(self):
218         return os.path.join(self.idp_storage_path,
219                             self.get_config_value('idp key file'))
220
221     @property
222     def default_allowed_nameids(self):
223         return self.get_config_value('default allowed nameids')
224
225     @property
226     def default_nameid(self):
227         return self.get_config_value('default nameid')
228
229     @property
230     def default_email_domain(self):
231         return self.get_config_value('default email domain')
232
233     def get_tree(self, site):
234         self.idp = self.init_idp()
235         self.page = SAML2(site, self)
236         self.admin = Saml2AdminPage(site, self)
237         return self.page
238
239     def init_idp(self):
240         idp = None
241         # Init IDP data
242         try:
243             idp = IdentityProvider(self)
244         except Exception, e:  # pylint: disable=broad-except
245             self._debug('Failed to init SAML2 provider: %r' % e)
246             return None
247
248         # Import all known applications
249         data = self.get_data()
250         for idval in data:
251             sp = data[idval]
252             if 'type' not in sp or sp['type'] != 'SP':
253                 continue
254             if 'name' not in sp or 'metadata' not in sp:
255                 continue
256             try:
257                 idp.add_provider(sp)
258             except Exception, e:  # pylint: disable=broad-except
259                 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
260
261         return idp
262
263     def on_enable(self):
264         super(IdpProvider, self).on_enable()
265         self.idp = self.init_idp()
266         if hasattr(self, 'admin'):
267             if self.admin:
268                 self.admin.add_sps()
269
270
271 class IdpMetadataGenerator(object):
272
273     def __init__(self, url, idp_cert, expiration=None):
274         self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
275         self.meta.set_entity_id('%s/saml2/metadata' % url)
276         self.meta.add_certs(idp_cert, idp_cert)
277         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
278                               '%s/saml2/SSO/POST' % url)
279         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
280                               '%s/saml2/SSO/Redirect' % url)
281         self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
282                               '%s/saml2/SLO/Redirect' % url)
283         self.meta.add_allowed_name_format(
284             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
285         self.meta.add_allowed_name_format(
286             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
287         self.meta.add_allowed_name_format(
288             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
289
290     def output(self, path=None):
291         return self.meta.output(path)
292
293
294 class Installer(object):
295
296     def __init__(self, *pargs):
297         self.name = 'saml2'
298         self.ptype = 'provider'
299         self.pargs = pargs
300
301     def install_args(self, group):
302         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
303                            help='Configure SAML2 Provider')
304
305     def configure(self, opts):
306         if opts['saml2'] != 'yes':
307             return
308
309         # Check storage path is present or create it
310         path = os.path.join(opts['data_dir'], 'saml2')
311         if not os.path.exists(path):
312             os.makedirs(path, 0700)
313
314         # Use the same cert for signing and ecnryption for now
315         cert = Certificate(path)
316         cert.generate('idp', opts['hostname'])
317
318         # Generate Idp Metadata
319         proto = 'https'
320         if opts['secure'].lower() == 'no':
321             proto = 'http'
322         url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
323         meta = IdpMetadataGenerator(url, cert,
324                                     timedelta(METADATA_VALIDITY_PERIOD))
325         if 'krb' in opts and opts['krb'] == 'yes':
326             meta.meta.add_allowed_name_format(
327                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
328
329         meta.output(os.path.join(path, 'metadata.xml'))
330
331         # Add configuration data to database
332         po = PluginObject(*self.pargs)
333         po.name = 'saml2'
334         po.wipe_data()
335         po.wipe_config_values()
336         config = {'idp storage path': path,
337                   'idp metadata file': 'metadata.xml',
338                   'idp certificate file': cert.cert,
339                   'idp key file': cert.key}
340         po.save_plugin_config(config)
341
342         # Update global config to add login plugin
343         po.is_enabled = True
344         po.save_enabled_state()
345
346         # Fixup permissions so only the ipsilon user can read these files
347         files.fix_user_dirs(path, opts['system_user'])