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