Add Metadata Generator helper class
[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 IdpMetadataGenerator(object):
246
247     def __init__(self, url, idp_cert):
248         self.meta = metadata.Metadata(metadata.IDP_ROLE)
249         self.meta.set_entity_id('%s/saml2/metadata' % url)
250         self.meta.add_certs(idp_cert, idp_cert)
251         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
252                               '%s/saml2/SSO/POST' % url)
253         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
254                               '%s/saml2/SSO/Redirect' % url)
255         self.meta.add_allowed_name_format(
256             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
257         self.meta.add_allowed_name_format(
258             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
259         self.meta.add_allowed_name_format(
260             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
261
262     def output(self, path=None):
263         return self.meta.output(path)
264
265
266 class Installer(object):
267
268     def __init__(self, *pargs):
269         self.name = 'saml2'
270         self.ptype = 'provider'
271         self.pargs = pargs
272
273     def install_args(self, group):
274         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
275                            help='Configure SAML2 Provider')
276
277     def configure(self, opts):
278         if opts['saml2'] != 'yes':
279             return
280
281         # Check storage path is present or create it
282         path = os.path.join(opts['data_dir'], 'saml2')
283         if not os.path.exists(path):
284             os.makedirs(path, 0700)
285
286         # Use the same cert for signing and ecnryption for now
287         cert = Certificate(path)
288         cert.generate('idp', opts['hostname'])
289
290         # Generate Idp Metadata
291         proto = 'https'
292         if opts['secure'].lower() == 'no':
293             proto = 'http'
294         url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
295         meta = IdpMetadataGenerator(url, cert)
296         if 'krb' in opts and opts['krb'] == 'yes':
297             meta.meta.add_allowed_name_format(
298                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
299
300         meta.output(os.path.join(path, 'metadata.xml'))
301
302         # Add configuration data to database
303         po = PluginObject(*self.pargs)
304         po.name = 'saml2'
305         po.wipe_data()
306         po.wipe_config_values()
307         config = {'idp storage path': path,
308                   'idp metadata file': 'metadata.xml',
309                   'idp certificate file': cert.cert,
310                   'idp key file': cert.key}
311         po.save_plugin_config(config)
312
313         # Update global config to add login plugin
314         po.is_enabled = True
315         po.save_enabled_state()
316
317         # Fixup permissions so only the ipsilon user can read these files
318         files.fix_user_dirs(path, opts['system_user'])