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