Use transactions throughout the code
[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 AdminPage
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 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):
122         super(IdpProvider, self).__init__('saml2', 'saml2')
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._options = {
130             'idp storage path': [
131                 """ Path to data storage accessible by the IdP """,
132                 'string',
133                 '/var/lib/ipsilon/saml2'
134             ],
135             'idp metadata file': [
136                 """ The IdP Metadata file genearated at install time. """,
137                 'string',
138                 'metadata.xml'
139             ],
140             'idp certificate file': [
141                 """ The IdP PEM Certificate genearated at install time. """,
142                 'string',
143                 'certificate.pem'
144             ],
145             'idp key file': [
146                 """ The IdP Certificate Key genearated at install time. """,
147                 'string',
148                 'certificate.key'
149             ],
150             'allow self registration': [
151                 """ Allow authenticated users to register applications. """,
152                 'boolean',
153                 True
154             ],
155             'default allowed nameids': [
156                 """Default Allowed NameIDs for Service Providers. """,
157                 'list',
158                 ['persistent', 'transient', 'email', 'kerberos', 'x509']
159             ],
160             'default nameid': [
161                 """Default NameID used by Service Providers. """,
162                 'string',
163                 'persistent'
164             ],
165             'default email domain': [
166                 """Default email domain, for users missing email property.""",
167                 'string',
168                 'example.com'
169             ]
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 = AdminPage(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.set_config(config)
311         po.save_plugin_config(FACILITY)
312
313         # Fixup permissions so only the ipsilon user can read these files
314         files.fix_user_dirs(path, opts['system_user'])