Refactor plugin initialization and enablement
[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.saml2.auth import AuthenticateRequest
22 from ipsilon.providers.saml2.admin import Saml2AdminPage
23 from ipsilon.providers.saml2.provider import IdentityProvider
24 from ipsilon.tools.certs import Certificate
25 from ipsilon.tools import saml2metadata as metadata
26 from ipsilon.tools import files
27 from ipsilon.util.user import UserSession
28 from ipsilon.util.plugin import PluginObject
29 from ipsilon.util import config as pconfig
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         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 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         self.SSO = SSO(*args, **kwargs)
117
118
119 class IdpProvider(ProviderBase):
120
121     def __init__(self, *pargs):
122         super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
123         self.admin = None
124         self.page = None
125         self.idp = None
126         self.description = """
127 Provides SAML 2.0 authentication infrastructure. """
128
129         self.new_config(
130             self.name,
131             pconfig.String(
132                 'idp storage path',
133                 'Path to data storage accessible by the IdP.',
134                 '/var/lib/ipsilon/saml2'),
135             pconfig.String(
136                 'idp metadata file',
137                 'The IdP Metadata file genearated at install time.',
138                 'metadata.xml'),
139             pconfig.String(
140                 'idp certificate file',
141                 'The IdP PEM Certificate genearated at install time.',
142                 'certificate.pem'),
143             pconfig.String(
144                 'idp key file',
145                 'The IdP Certificate Key genearated at install time.',
146                 'certificate.key'),
147             pconfig.Condition(
148                 'allow self registration',
149                 'Allow authenticated users to register applications.',
150                 True),
151             pconfig.Choice(
152                 'default allowed nameids',
153                 'Default Allowed NameIDs for Service Providers.',
154                 metadata.SAML2_NAMEID_MAP.keys(),
155                 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
156             pconfig.Pick(
157                 'default nameid',
158                 'Default NameID used by Service Providers.',
159                 metadata.SAML2_NAMEID_MAP.keys(),
160                 'persistent'),
161             pconfig.String(
162                 'default email domain',
163                 'Used for users missing the email property.',
164                 'example.com'),
165         )
166         if cherrypy.config.get('debug', False):
167             import logging
168             import sys
169             logger = logging.getLogger('lasso')
170             lh = logging.StreamHandler(sys.stderr)
171             logger.addHandler(lh)
172             logger.setLevel(logging.DEBUG)
173
174     @property
175     def allow_self_registration(self):
176         return self.get_config_value('allow self registration')
177
178     @property
179     def idp_storage_path(self):
180         return self.get_config_value('idp storage path')
181
182     @property
183     def idp_metadata_file(self):
184         return os.path.join(self.idp_storage_path,
185                             self.get_config_value('idp metadata file'))
186
187     @property
188     def idp_certificate_file(self):
189         return os.path.join(self.idp_storage_path,
190                             self.get_config_value('idp certificate file'))
191
192     @property
193     def idp_key_file(self):
194         return os.path.join(self.idp_storage_path,
195                             self.get_config_value('idp key file'))
196
197     @property
198     def default_allowed_nameids(self):
199         return self.get_config_value('default allowed nameids')
200
201     @property
202     def default_nameid(self):
203         return self.get_config_value('default nameid')
204
205     @property
206     def default_email_domain(self):
207         return self.get_config_value('default email domain')
208
209     def get_tree(self, site):
210         self.idp = self.init_idp()
211         self.page = SAML2(site, self)
212         self.admin = Saml2AdminPage(site, self)
213         return self.page
214
215     def init_idp(self):
216         idp = None
217         # Init IDP data
218         try:
219             idp = IdentityProvider(self)
220         except Exception, e:  # pylint: disable=broad-except
221             self._debug('Failed to init SAML2 provider: %r' % e)
222             return None
223
224         # Import all known applications
225         data = self.get_data()
226         for idval in data:
227             sp = data[idval]
228             if 'type' not in sp or sp['type'] != 'SP':
229                 continue
230             if 'name' not in sp or 'metadata' not in sp:
231                 continue
232             try:
233                 idp.add_provider(sp)
234             except Exception, e:  # pylint: disable=broad-except
235                 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
236
237         return idp
238
239     def on_enable(self):
240         super(IdpProvider, self).on_enable()
241         self.idp = self.init_idp()
242         if hasattr(self, 'admin'):
243             if self.admin:
244                 self.admin.add_sps()
245
246
247 class Installer(object):
248
249     def __init__(self, *pargs):
250         self.name = 'saml2'
251         self.ptype = 'provider'
252         self.pargs = pargs
253
254     def install_args(self, group):
255         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
256                            help='Configure SAML2 Provider')
257
258     def configure(self, opts):
259         if opts['saml2'] != 'yes':
260             return
261
262         # Check storage path is present or create it
263         path = os.path.join(opts['data_dir'], 'saml2')
264         if not os.path.exists(path):
265             os.makedirs(path, 0700)
266
267         # Use the same cert for signing and ecnryption for now
268         cert = Certificate(path)
269         cert.generate('idp', opts['hostname'])
270
271         # Generate Idp Metadata
272         proto = 'https'
273         if opts['secure'].lower() == 'no':
274             proto = 'http'
275         url = '%s://%s/%s/saml2' % (proto, opts['hostname'], opts['instance'])
276         meta = metadata.Metadata(metadata.IDP_ROLE)
277         meta.set_entity_id(url + '/metadata')
278         meta.add_certs(cert, cert)
279         meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
280                          url + '/SSO/POST')
281         meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
282                          url + '/SSO/Redirect')
283
284         meta.add_allowed_name_format(
285             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
286         meta.add_allowed_name_format(
287             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
288         meta.add_allowed_name_format(
289             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
290         if 'krb' in opts and opts['krb'] == 'yes':
291             meta.add_allowed_name_format(
292                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
293
294         meta.output(os.path.join(path, 'metadata.xml'))
295
296         # Add configuration data to database
297         po = PluginObject(*self.pargs)
298         po.name = 'saml2'
299         po.wipe_data()
300         po.wipe_config_values()
301         config = {'idp storage path': path,
302                   'idp metadata file': 'metadata.xml',
303                   'idp certificate file': cert.cert,
304                   'idp key file': cert.key}
305         po.save_plugin_config(config)
306
307         # Update global config to add login plugin
308         po.is_enabled = True
309         po.save_enabled_state()
310
311         # Fixup permissions so only the ipsilon user can read these files
312         files.fix_user_dirs(path, opts['system_user'])