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