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