Add saml2 configuration 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.certs import Certificate
24 from ipsilon.providers.saml2 import metadata
25 from ipsilon.util.user import UserSession
26 from ipsilon.util.plugin import PluginObject
27 import cherrypy
28 import lasso
29 import pwd
30 import os
31
32
33 class Redirect(AuthenticateRequest):
34
35     def GET(self, *args, **kwargs):
36
37         query = cherrypy.request.query_string
38
39         login = self.saml2login(query)
40         return self.auth(login)
41
42
43 class POSTAuth(AuthenticateRequest):
44
45     def POST(self, *args, **kwargs):
46
47         request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
48         relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
49
50         login = self.saml2login(request)
51         login.set_msgRelayState(relaystate)
52         return self.auth(login)
53
54
55 class Continue(AuthenticateRequest):
56
57     def GET(self, *args, **kwargs):
58
59         session = UserSession()
60         user = session.get_user()
61         session.nuke_data('login', 'Return')
62         self.stage = session.get_data('saml2', 'stage')
63
64         if user.is_anonymous:
65             self._debug("User is marked anonymous?!")
66             # TODO: Return to SP with auth failed error
67             raise cherrypy.HTTPError(401)
68
69         self._debug('Continue auth for %s' % user.name)
70
71         dump = session.get_data('saml2', 'Request')
72         if not dump:
73             self._debug("Couldn't find Request dump?!")
74             # TODO: Return to SP with auth failed error
75             raise cherrypy.HTTPError(400)
76
77         try:
78             login = lasso.Login.newFromDump(self.cfg.idp, dump)
79         except Exception, e:  # pylint: disable=broad-except
80             self._debug('Failed to load status from dump: %r' % e)
81
82         if not login:
83             self._debug("Empty Request dump?!")
84             # TODO: Return to SP with auth failed error
85             raise cherrypy.HTTPError(400)
86
87         return self.auth(login)
88
89
90 class SSO(ProviderPageBase):
91
92     def __init__(self, *args, **kwargs):
93         super(SSO, self).__init__(*args, **kwargs)
94         self.Redirect = Redirect(*args, **kwargs)
95         self.POST = POSTAuth(*args, **kwargs)
96         self.Continue = Continue(*args, **kwargs)
97
98
99 class SAML2(ProviderPageBase):
100
101     def __init__(self, *args, **kwargs):
102         super(SAML2, self).__init__(*args, **kwargs)
103
104         # Init IDP data
105         try:
106             self.cfg.idp = lasso.Server(self.cfg.idp_metadata_file,
107                                         self.cfg.idp_key_file,
108                                         None,
109                                         self.cfg.idp_certificate_file)
110             self.cfg.idp.role = lasso.PROVIDER_ROLE_IDP
111         except Exception, e:  # pylint: disable=broad-except
112             self._debug('Failed to enable SAML2 provider: %r' % e)
113             return
114
115         # Import all known applications
116         data = self.cfg.get_data()
117         for idval in data:
118             if 'type' not in data[idval] or data[idval]['type'] != 'SP':
119                 continue
120             path = os.path.join(self.cfg.idp_storage_path, str(idval))
121             sp = data[idval]
122             if 'name' in sp:
123                 name = sp['name']
124             else:
125                 name = str(idval)
126             try:
127                 meta = os.path.join(path, 'metadata.xml')
128                 cert = os.path.join(path, 'certificate.pem')
129                 self.cfg.idp.addProvider(lasso.PROVIDER_ROLE_SP, meta, cert)
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         return self.page
226
227
228 class Installer(object):
229
230     def __init__(self):
231         self.name = 'saml2'
232         self.ptype = 'provider'
233
234     def install_args(self, group):
235         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
236                            help='Configure SAML2 Provider')
237         group.add_argument('--saml2-storage',
238                            default='/var/lib/ipsilon/saml2',
239                            help='SAML2 Provider storage area')
240
241     def configure(self, opts):
242         if opts['saml2'] != 'yes':
243             return
244
245         # Check storage path is present or create it
246         path = opts['saml2_storage']
247         if not os.path.exists(path):
248             os.makedirs(path, 0700)
249
250         # Use the same cert for signing and ecnryption for now
251         cert = Certificate(path)
252         cert.generate('idp', opts['hostname'])
253
254         # Generate Idp Metadata
255         url = 'https://' + opts['hostname'] + '/idp/saml2'
256         meta = metadata.Metadata(metadata.IDP_ROLE)
257         meta.set_entity_id(url + '/metadata')
258         meta.add_certs(cert, cert)
259         meta.add_service(metadata.SSO_SERVICE,
260                          lasso.SAML2_METADATA_BINDING_POST,
261                          url + '/POST')
262         meta.add_service(metadata.SSO_SERVICE,
263                          lasso.SAML2_METADATA_BINDING_REDIRECT,
264                          url + '/Redirect')
265
266         meta.add_allowed_name_format(
267             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
268         meta.add_allowed_name_format(
269             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
270         meta.add_allowed_name_format(
271             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
272         if 'krb' in opts and opts['krb'] == 'yes':
273             meta.add_allowed_name_format(
274                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
275
276         meta.output(os.path.join(path, 'metadata.xml'))
277
278         # Add configuration data to database
279         po = PluginObject()
280         po.name = 'saml2'
281         po.wipe_data()
282
283         po.wipe_config_values(FACILITY)
284         config = {'idp storage path': path,
285                   'idp metadata file': 'metadata.xml',
286                   'idp certificate file': cert.cert,
287                   'idp key file': cert.key}
288         po.set_config(config)
289         po.save_plugin_config(FACILITY)
290
291         # Fixup permissions so only the ipsilon user can read these files
292         pw = pwd.getpwnam(opts['system_user'])
293         for root, dirs, files in os.walk(path):
294             for name in dirs:
295                 target = os.path.join(root, name)
296                 os.chown(target, pw.pw_uid, pw.pw_gid)
297                 os.chmod(target, 0700)
298             for name in files:
299                 target = os.path.join(root, name)
300                 os.chown(target, pw.pw_uid, pw.pw_gid)
301                 os.chmod(target, 0600)