507bba291491f164c0c51e8b449ae47085fdb975
[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.util.user import UserSession
28 from ipsilon.util.plugin import PluginObject
29 import cherrypy
30 import lasso
31 import pwd
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.page = None
145         self.idp = None
146         self.description = """
147 Provides SAML 2.0 authentication infrastructure. """
148
149         self._options = {
150             'idp storage path': [
151                 """ Path to data storage accessible by the IdP """,
152                 'string',
153                 '/var/lib/ipsilon/saml2'
154             ],
155             'idp metadata file': [
156                 """ The IdP Metadata file genearated at install time. """,
157                 'string',
158                 'metadata.xml'
159             ],
160             'idp certificate file': [
161                 """ The IdP PEM Certificate genearated at install time. """,
162                 'string',
163                 'certificate.pem'
164             ],
165             'idp key file': [
166                 """ The IdP Certificate Key genearated at install time. """,
167                 'string',
168                 'certificate.key'
169             ],
170             'allow self registration': [
171                 """ Allow authenticated users to register applications. """,
172                 'boolean',
173                 True
174             ],
175             'default allowed nameids': [
176                 """Default Allowed NameIDs for Service Providers. """,
177                 'list',
178                 ['persistent', 'transient', 'email', 'kerberos', 'x509']
179             ],
180             'default nameid': [
181                 """Default NameID used by Service Providers. """,
182                 'string',
183                 'persistent'
184             ],
185             'default email domain': [
186                 """Default email domain, for users missing email property.""",
187                 'string',
188                 'example.com'
189             ]
190         }
191         if cherrypy.config.get('debug', False):
192             import logging
193             import sys
194             logger = logging.getLogger('lasso')
195             lh = logging.StreamHandler(sys.stderr)
196             logger.addHandler(lh)
197             logger.setLevel(logging.DEBUG)
198
199     @property
200     def allow_self_registration(self):
201         return self.get_config_value('allow self registration')
202
203     @property
204     def idp_storage_path(self):
205         return self.get_config_value('idp storage path')
206
207     @property
208     def idp_metadata_file(self):
209         return os.path.join(self.idp_storage_path,
210                             self.get_config_value('idp metadata file'))
211
212     @property
213     def idp_certificate_file(self):
214         return os.path.join(self.idp_storage_path,
215                             self.get_config_value('idp certificate file'))
216
217     @property
218     def idp_key_file(self):
219         return os.path.join(self.idp_storage_path,
220                             self.get_config_value('idp key file'))
221
222     @property
223     def default_allowed_nameids(self):
224         return self.get_config_value('default allowed nameids')
225
226     @property
227     def default_nameid(self):
228         return self.get_config_value('default nameid')
229
230     @property
231     def default_email_domain(self):
232         return self.get_config_value('default email domain')
233
234     def get_tree(self, site):
235         self.page = SAML2(site, self)
236         self.admin = AdminPage(site, self)
237         return self.page
238
239
240 class Installer(object):
241
242     def __init__(self):
243         self.name = 'saml2'
244         self.ptype = 'provider'
245
246     def install_args(self, group):
247         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
248                            help='Configure SAML2 Provider')
249         group.add_argument('--saml2-storage',
250                            default='/var/lib/ipsilon/saml2',
251                            help='SAML2 Provider storage area')
252
253     def configure(self, opts):
254         if opts['saml2'] != 'yes':
255             return
256
257         # Check storage path is present or create it
258         path = opts['saml2_storage']
259         if not os.path.exists(path):
260             os.makedirs(path, 0700)
261
262         # Use the same cert for signing and ecnryption for now
263         cert = Certificate(path)
264         cert.generate('idp', opts['hostname'])
265
266         # Generate Idp Metadata
267         url = 'https://' + opts['hostname'] + '/idp/saml2'
268         meta = metadata.Metadata(metadata.IDP_ROLE)
269         meta.set_entity_id(url + '/metadata')
270         meta.add_certs(cert, cert)
271         meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
272                          url + 'SSO/POST')
273         meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
274                          url + 'SSO/Redirect')
275
276         meta.add_allowed_name_format(
277             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
278         meta.add_allowed_name_format(
279             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
280         meta.add_allowed_name_format(
281             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
282         if 'krb' in opts and opts['krb'] == 'yes':
283             meta.add_allowed_name_format(
284                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
285
286         meta.output(os.path.join(path, 'metadata.xml'))
287
288         # Add configuration data to database
289         po = PluginObject()
290         po.name = 'saml2'
291         po.wipe_data()
292
293         po.wipe_config_values(FACILITY)
294         config = {'idp storage path': path,
295                   'idp metadata file': 'metadata.xml',
296                   'idp certificate file': cert.cert,
297                   'idp key file': cert.key}
298         po.set_config(config)
299         po.save_plugin_config(FACILITY)
300
301         # Fixup permissions so only the ipsilon user can read these files
302         pw = pwd.getpwnam(opts['system_user'])
303         for root, dirs, files in os.walk(path):
304             for name in dirs:
305                 target = os.path.join(root, name)
306                 os.chown(target, pw.pw_uid, pw.pw_gid)
307                 os.chmod(target, 0700)
308             for name in files:
309                 target = os.path.join(root, name)
310                 os.chown(target, pw.pw_uid, pw.pw_gid)
311                 os.chmod(target, 0600)