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