Load and initialize REST in the SAML2 plugin
[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     ProviderInstaller
20 from ipsilon.providers.saml2.auth import AuthenticateRequest
21 from ipsilon.providers.saml2.logout import LogoutRequest
22 from ipsilon.providers.saml2.admin import Saml2AdminPage
23 from ipsilon.providers.saml2.rest import Saml2RestBase
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 from ipsilon.util import config as pconfig
31 import cherrypy
32 from datetime import timedelta
33 import lasso
34 import os
35 import time
36
37
38 class Redirect(AuthenticateRequest):
39
40     def GET(self, *args, **kwargs):
41
42         query = cherrypy.request.query_string
43
44         login = self.saml2login(query)
45         return self.auth(login)
46
47
48 class POSTAuth(AuthenticateRequest):
49
50     def POST(self, *args, **kwargs):
51
52         request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
53         relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
54
55         login = self.saml2login(request)
56         login.set_msgRelayState(relaystate)
57         return self.auth(login)
58
59
60 class Continue(AuthenticateRequest):
61
62     def GET(self, *args, **kwargs):
63
64         session = UserSession()
65         user = session.get_user()
66         transdata = self.trans.retrieve()
67         self.stage = transdata['saml2_stage']
68
69         if user.is_anonymous:
70             self._debug("User is marked anonymous?!")
71             # TODO: Return to SP with auth failed error
72             raise cherrypy.HTTPError(401)
73
74         self._debug('Continue auth for %s' % user.name)
75
76         if 'saml2_request' not in transdata:
77             self._debug("Couldn't find Request dump?!")
78             # TODO: Return to SP with auth failed error
79             raise cherrypy.HTTPError(400)
80         dump = transdata['saml2_request']
81
82         try:
83             login = self.cfg.idp.get_login_handler(dump)
84         except Exception, e:  # pylint: disable=broad-except
85             self._debug('Failed to load status from dump: %r' % e)
86
87         if not login:
88             self._debug("Empty Request dump?!")
89             # TODO: Return to SP with auth failed error
90             raise cherrypy.HTTPError(400)
91
92         return self.auth(login)
93
94
95 class RedirectLogout(LogoutRequest):
96
97     def GET(self, *args, **kwargs):
98         query = cherrypy.request.query_string
99
100         relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
101         response = kwargs.get(lasso.SAML2_FIELD_RESPONSE)
102
103         return self.logout(query,
104                            relaystate=relaystate,
105                            samlresponse=response)
106
107
108 class SSO(ProviderPageBase):
109
110     def __init__(self, *args, **kwargs):
111         super(SSO, self).__init__(*args, **kwargs)
112         self.Redirect = Redirect(*args, **kwargs)
113         self.POST = POSTAuth(*args, **kwargs)
114         self.Continue = Continue(*args, **kwargs)
115
116
117 class SLO(ProviderPageBase):
118
119     def __init__(self, *args, **kwargs):
120         super(SLO, self).__init__(*args, **kwargs)
121         self._debug('SLO init')
122         self.Redirect = RedirectLogout(*args, **kwargs)
123
124
125 # one week
126 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
127 # 30 days
128 METADATA_VALIDITY_PERIOD = 30
129
130
131 class Metadata(ProviderPageBase):
132     def GET(self, *args, **kwargs):
133
134         body = self._get_metadata()
135         cherrypy.response.headers["Content-Type"] = "text/xml"
136         cherrypy.response.headers["Content-Disposition"] = \
137             'attachment; filename="metadata.xml"'
138         return body
139
140     def _get_metadata(self):
141         if os.path.isfile(self.cfg.idp_metadata_file):
142             s = os.stat(self.cfg.idp_metadata_file)
143             if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
144                 with open(self.cfg.idp_metadata_file) as m:
145                     return m.read()
146
147         # Otherwise generate and save
148         idp_cert = Certificate()
149         idp_cert.import_cert(self.cfg.idp_certificate_file,
150                              self.cfg.idp_key_file)
151         meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
152                                     timedelta(METADATA_VALIDITY_PERIOD))
153         body = meta.output()
154         with open(self.cfg.idp_metadata_file, 'w+') as m:
155             m.write(body)
156         return body
157
158
159 class SAML2(ProviderPageBase):
160
161     def __init__(self, *args, **kwargs):
162         super(SAML2, self).__init__(*args, **kwargs)
163         self.metadata = Metadata(*args, **kwargs)
164         self.SSO = SSO(*args, **kwargs)
165         self.SLO = SLO(*args, **kwargs)
166
167
168 class IdpProvider(ProviderBase):
169
170     def __init__(self, *pargs):
171         super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
172         self.admin = None
173         self.rest = None
174         self.page = None
175         self.idp = None
176         self.description = """
177 Provides SAML 2.0 authentication infrastructure. """
178
179         self.new_config(
180             self.name,
181             pconfig.String(
182                 'idp storage path',
183                 'Path to data storage accessible by the IdP.',
184                 '/var/lib/ipsilon/saml2'),
185             pconfig.String(
186                 'idp metadata file',
187                 'The IdP Metadata file genearated at install time.',
188                 'metadata.xml'),
189             pconfig.String(
190                 'idp certificate file',
191                 'The IdP PEM Certificate genearated at install time.',
192                 'certificate.pem'),
193             pconfig.String(
194                 'idp key file',
195                 'The IdP Certificate Key genearated at install time.',
196                 'certificate.key'),
197             pconfig.Condition(
198                 'allow self registration',
199                 'Allow authenticated users to register applications.',
200                 True),
201             pconfig.Choice(
202                 'default allowed nameids',
203                 'Default Allowed NameIDs for Service Providers.',
204                 metadata.SAML2_NAMEID_MAP.keys(),
205                 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
206             pconfig.Pick(
207                 'default nameid',
208                 'Default NameID used by Service Providers.',
209                 metadata.SAML2_NAMEID_MAP.keys(),
210                 'persistent'),
211             pconfig.String(
212                 'default email domain',
213                 'Used for users missing the email property.',
214                 'example.com'),
215             pconfig.MappingList(
216                 'default attribute mapping',
217                 'Defines how to map attributes before returning them to SPs',
218                 [['*', '*']]),
219             pconfig.ComplexList(
220                 'default allowed attributes',
221                 'Defines a list of allowed attributes, applied after mapping',
222                 ['*']),
223         )
224         if cherrypy.config.get('debug', False):
225             import logging
226             import sys
227             logger = logging.getLogger('lasso')
228             lh = logging.StreamHandler(sys.stderr)
229             logger.addHandler(lh)
230             logger.setLevel(logging.DEBUG)
231
232     @property
233     def allow_self_registration(self):
234         return self.get_config_value('allow self registration')
235
236     @property
237     def idp_storage_path(self):
238         return self.get_config_value('idp storage path')
239
240     @property
241     def idp_metadata_file(self):
242         return os.path.join(self.idp_storage_path,
243                             self.get_config_value('idp metadata file'))
244
245     @property
246     def idp_certificate_file(self):
247         return os.path.join(self.idp_storage_path,
248                             self.get_config_value('idp certificate file'))
249
250     @property
251     def idp_key_file(self):
252         return os.path.join(self.idp_storage_path,
253                             self.get_config_value('idp key file'))
254
255     @property
256     def default_allowed_nameids(self):
257         return self.get_config_value('default allowed nameids')
258
259     @property
260     def default_nameid(self):
261         return self.get_config_value('default nameid')
262
263     @property
264     def default_email_domain(self):
265         return self.get_config_value('default email domain')
266
267     @property
268     def default_attribute_mapping(self):
269         return self.get_config_value('default attribute mapping')
270
271     @property
272     def default_allowed_attributes(self):
273         return self.get_config_value('default allowed attributes')
274
275     def get_tree(self, site):
276         self.idp = self.init_idp()
277         self.page = SAML2(site, self)
278         self.admin = Saml2AdminPage(site, self)
279         self.rest = Saml2RestBase(site, self)
280         return self.page
281
282     def init_idp(self):
283         idp = None
284         # Init IDP data
285         try:
286             idp = IdentityProvider(self)
287         except Exception, e:  # pylint: disable=broad-except
288             self._debug('Failed to init SAML2 provider: %r' % e)
289             return None
290
291         # Import all known applications
292         data = self.get_data()
293         for idval in data:
294             sp = data[idval]
295             if 'type' not in sp or sp['type'] != 'SP':
296                 continue
297             if 'name' not in sp or 'metadata' not in sp:
298                 continue
299             try:
300                 idp.add_provider(sp)
301             except Exception, e:  # pylint: disable=broad-except
302                 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
303
304         return idp
305
306     def on_enable(self):
307         super(IdpProvider, self).on_enable()
308         self.idp = self.init_idp()
309         if hasattr(self, 'admin'):
310             if self.admin:
311                 self.admin.add_sps()
312
313
314 class IdpMetadataGenerator(object):
315
316     def __init__(self, url, idp_cert, expiration=None):
317         self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
318         self.meta.set_entity_id('%s/saml2/metadata' % url)
319         self.meta.add_certs(idp_cert, idp_cert)
320         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
321                               '%s/saml2/SSO/POST' % url)
322         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
323                               '%s/saml2/SSO/Redirect' % url)
324         self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
325                               '%s/saml2/SLO/Redirect' % url)
326         self.meta.add_allowed_name_format(
327             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
328         self.meta.add_allowed_name_format(
329             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
330         self.meta.add_allowed_name_format(
331             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
332
333     def output(self, path=None):
334         return self.meta.output(path)
335
336
337 class Installer(ProviderInstaller):
338
339     def __init__(self, *pargs):
340         super(Installer, self).__init__()
341         self.name = 'saml2'
342         self.pargs = pargs
343
344     def install_args(self, group):
345         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
346                            help='Configure SAML2 Provider')
347
348     def configure(self, opts):
349         if opts['saml2'] != 'yes':
350             return
351
352         # Check storage path is present or create it
353         path = os.path.join(opts['data_dir'], 'saml2')
354         if not os.path.exists(path):
355             os.makedirs(path, 0700)
356
357         # Use the same cert for signing and ecnryption for now
358         cert = Certificate(path)
359         cert.generate('idp', opts['hostname'])
360
361         # Generate Idp Metadata
362         proto = 'https'
363         if opts['secure'].lower() == 'no':
364             proto = 'http'
365         url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
366         meta = IdpMetadataGenerator(url, cert,
367                                     timedelta(METADATA_VALIDITY_PERIOD))
368         if 'krb' in opts and opts['krb'] == 'yes':
369             meta.meta.add_allowed_name_format(
370                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
371
372         meta.output(os.path.join(path, 'metadata.xml'))
373
374         # Add configuration data to database
375         po = PluginObject(*self.pargs)
376         po.name = 'saml2'
377         po.wipe_data()
378         po.wipe_config_values()
379         config = {'idp storage path': path,
380                   'idp metadata file': 'metadata.xml',
381                   'idp certificate file': cert.cert,
382                   'idp key file': cert.key}
383         po.save_plugin_config(config)
384
385         # Update global config to add login plugin
386         po.is_enabled = True
387         po.save_enabled_state()
388
389         # Fixup permissions so only the ipsilon user can read these files
390         files.fix_user_dirs(path, opts['system_user'])