Return Idps metadata file on request
[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 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
192     @property
193     def allow_self_registration(self):
194         return self.get_config_value('allow self registration')
195
196     @property
197     def idp_storage_path(self):
198         return self.get_config_value('idp storage path')
199
200     @property
201     def idp_metadata_file(self):
202         return os.path.join(self.idp_storage_path,
203                             self.get_config_value('idp metadata file'))
204
205     @property
206     def idp_certificate_file(self):
207         return os.path.join(self.idp_storage_path,
208                             self.get_config_value('idp certificate file'))
209
210     @property
211     def idp_key_file(self):
212         return os.path.join(self.idp_storage_path,
213                             self.get_config_value('idp key file'))
214
215     @property
216     def default_allowed_nameids(self):
217         return self.get_config_value('default allowed nameids')
218
219     @property
220     def default_nameid(self):
221         return self.get_config_value('default nameid')
222
223     @property
224     def default_email_domain(self):
225         return self.get_config_value('default email domain')
226
227     def get_tree(self, site):
228         self.page = SAML2(site, self)
229         self.admin = AdminPage(site, self)
230         return self.page
231
232
233 class Installer(object):
234
235     def __init__(self):
236         self.name = 'saml2'
237         self.ptype = 'provider'
238
239     def install_args(self, group):
240         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
241                            help='Configure SAML2 Provider')
242         group.add_argument('--saml2-storage',
243                            default='/var/lib/ipsilon/saml2',
244                            help='SAML2 Provider storage area')
245
246     def configure(self, opts):
247         if opts['saml2'] != 'yes':
248             return
249
250         # Check storage path is present or create it
251         path = opts['saml2_storage']
252         if not os.path.exists(path):
253             os.makedirs(path, 0700)
254
255         # Use the same cert for signing and ecnryption for now
256         cert = Certificate(path)
257         cert.generate('idp', opts['hostname'])
258
259         # Generate Idp Metadata
260         url = 'https://' + opts['hostname'] + '/idp/saml2'
261         meta = metadata.Metadata(metadata.IDP_ROLE)
262         meta.set_entity_id(url + '/metadata')
263         meta.add_certs(cert, cert)
264         meta.add_service(metadata.SSO_SERVICE,
265                          lasso.SAML2_METADATA_BINDING_POST,
266                          url + 'SSO/POST')
267         meta.add_service(metadata.SSO_SERVICE,
268                          lasso.SAML2_METADATA_BINDING_REDIRECT,
269                          url + 'SSO/Redirect')
270
271         meta.add_allowed_name_format(
272             lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
273         meta.add_allowed_name_format(
274             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
275         meta.add_allowed_name_format(
276             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
277         if 'krb' in opts and opts['krb'] == 'yes':
278             meta.add_allowed_name_format(
279                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
280
281         meta.output(os.path.join(path, 'metadata.xml'))
282
283         # Add configuration data to database
284         po = PluginObject()
285         po.name = 'saml2'
286         po.wipe_data()
287
288         po.wipe_config_values(FACILITY)
289         config = {'idp storage path': path,
290                   'idp metadata file': 'metadata.xml',
291                   'idp certificate file': cert.cert,
292                   'idp key file': cert.key}
293         po.set_config(config)
294         po.save_plugin_config(FACILITY)
295
296         # Fixup permissions so only the ipsilon user can read these files
297         pw = pwd.getpwnam(opts['system_user'])
298         for root, dirs, files in os.walk(path):
299             for name in dirs:
300                 target = os.path.join(root, name)
301                 os.chown(target, pw.pw_uid, pw.pw_gid)
302                 os.chmod(target, 0700)
303             for name in files:
304                 target = os.path.join(root, name)
305                 os.chown(target, pw.pw_uid, pw.pw_gid)
306                 os.chmod(target, 0600)