1 # Copyright (C) 2014 Simo Sorce <simo@redhat.com>
3 # see file 'COPYING' for use and warranty information
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
20 from ipsilon.providers.saml2.auth import AuthenticateRequest
21 from ipsilon.providers.saml2.logout import LogoutRequest
22 from ipsilon.providers.saml2.admin import Saml2AdminPage
23 from ipsilon.providers.saml2.rest import Saml2RestBase
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.tools import files
28 from ipsilon.util.user import UserSession
29 from ipsilon.util.plugin import PluginObject
30 from ipsilon.util import config as pconfig
32 from datetime import timedelta
39 class Redirect(AuthenticateRequest):
41 def GET(self, *args, **kwargs):
43 query = cherrypy.request.query_string
45 login = self.saml2login(query)
46 return self.auth(login)
49 class POSTAuth(AuthenticateRequest):
51 def POST(self, *args, **kwargs):
53 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
54 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
56 login = self.saml2login(request)
57 login.set_msgRelayState(relaystate)
58 return self.auth(login)
61 class Continue(AuthenticateRequest):
63 def GET(self, *args, **kwargs):
65 session = UserSession()
66 user = session.get_user()
67 transdata = self.trans.retrieve()
68 self.stage = transdata['saml2_stage']
71 self._debug("User is marked anonymous?!")
72 # TODO: Return to SP with auth failed error
73 raise cherrypy.HTTPError(401)
75 self._debug('Continue auth for %s' % user.name)
77 if 'saml2_request' not in transdata:
78 self._debug("Couldn't find Request dump?!")
79 # TODO: Return to SP with auth failed error
80 raise cherrypy.HTTPError(400)
81 dump = transdata['saml2_request']
84 login = self.cfg.idp.get_login_handler(dump)
85 except Exception, e: # pylint: disable=broad-except
86 self._debug('Failed to load status from dump: %r' % e)
89 self._debug("Empty Request dump?!")
90 # TODO: Return to SP with auth failed error
91 raise cherrypy.HTTPError(400)
93 return self.auth(login)
96 class RedirectLogout(LogoutRequest):
98 def GET(self, *args, **kwargs):
99 query = cherrypy.request.query_string
101 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
102 response = kwargs.get(lasso.SAML2_FIELD_RESPONSE)
104 return self.logout(query,
105 relaystate=relaystate,
106 samlresponse=response)
109 class SSO(ProviderPageBase):
111 def __init__(self, *args, **kwargs):
112 super(SSO, self).__init__(*args, **kwargs)
113 self.Redirect = Redirect(*args, **kwargs)
114 self.POST = POSTAuth(*args, **kwargs)
115 self.Continue = Continue(*args, **kwargs)
118 class SLO(ProviderPageBase):
120 def __init__(self, *args, **kwargs):
121 super(SLO, self).__init__(*args, **kwargs)
122 self._debug('SLO init')
123 self.Redirect = RedirectLogout(*args, **kwargs)
127 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
129 METADATA_VALIDITY_PERIOD = 30
132 class Metadata(ProviderPageBase):
133 def GET(self, *args, **kwargs):
135 body = self._get_metadata()
136 cherrypy.response.headers["Content-Type"] = "text/xml"
137 cherrypy.response.headers["Content-Disposition"] = \
138 'attachment; filename="metadata.xml"'
141 def _get_metadata(self):
142 if os.path.isfile(self.cfg.idp_metadata_file):
143 s = os.stat(self.cfg.idp_metadata_file)
144 if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
145 with open(self.cfg.idp_metadata_file) as m:
148 # Otherwise generate and save
149 idp_cert = Certificate()
150 idp_cert.import_cert(self.cfg.idp_certificate_file,
151 self.cfg.idp_key_file)
152 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
153 timedelta(METADATA_VALIDITY_PERIOD))
155 with open(self.cfg.idp_metadata_file, 'w+') as m:
160 class SAML2(ProviderPageBase):
162 def __init__(self, *args, **kwargs):
163 super(SAML2, self).__init__(*args, **kwargs)
164 self.metadata = Metadata(*args, **kwargs)
165 self.SSO = SSO(*args, **kwargs)
166 self.SLO = SLO(*args, **kwargs)
169 class IdpProvider(ProviderBase):
171 def __init__(self, *pargs):
172 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
177 self.description = """
178 Provides SAML 2.0 authentication infrastructure. """
184 'Path to data storage accessible by the IdP.',
185 '/var/lib/ipsilon/saml2'),
188 'The IdP Metadata file genearated at install time.',
191 'idp certificate file',
192 'The IdP PEM Certificate genearated at install time.',
196 'The IdP Certificate Key genearated at install time.',
200 'The salt used for persistent Name IDs.',
203 'allow self registration',
204 'Allow authenticated users to register applications.',
207 'default allowed nameids',
208 'Default Allowed NameIDs for Service Providers.',
209 metadata.SAML2_NAMEID_MAP.keys(),
210 ['unspecified', 'persistent', 'transient', 'email',
211 'kerberos', 'x509']),
214 'Default NameID used by Service Providers.',
215 metadata.SAML2_NAMEID_MAP.keys(),
218 'default email domain',
219 'Used for users missing the email property.',
222 'default attribute mapping',
223 'Defines how to map attributes before returning them to SPs',
226 'default allowed attributes',
227 'Defines a list of allowed attributes, applied after mapping',
230 if cherrypy.config.get('debug', False):
233 logger = logging.getLogger('lasso')
234 lh = logging.StreamHandler(sys.stderr)
235 logger.addHandler(lh)
236 logger.setLevel(logging.DEBUG)
239 def allow_self_registration(self):
240 return self.get_config_value('allow self registration')
243 def idp_storage_path(self):
244 return self.get_config_value('idp storage path')
247 def idp_metadata_file(self):
248 return os.path.join(self.idp_storage_path,
249 self.get_config_value('idp metadata file'))
252 def idp_certificate_file(self):
253 return os.path.join(self.idp_storage_path,
254 self.get_config_value('idp certificate file'))
257 def idp_key_file(self):
258 return os.path.join(self.idp_storage_path,
259 self.get_config_value('idp key file'))
262 def idp_nameid_salt(self):
263 return self.get_config_value('idp nameid salt')
266 def default_allowed_nameids(self):
267 return self.get_config_value('default allowed nameids')
270 def default_nameid(self):
271 return self.get_config_value('default nameid')
274 def default_email_domain(self):
275 return self.get_config_value('default email domain')
278 def default_attribute_mapping(self):
279 return self.get_config_value('default attribute mapping')
282 def default_allowed_attributes(self):
283 return self.get_config_value('default allowed attributes')
285 def get_tree(self, site):
286 self.idp = self.init_idp()
287 self.page = SAML2(site, self)
288 self.admin = Saml2AdminPage(site, self)
289 self.rest = Saml2RestBase(site, self)
296 idp = IdentityProvider(self)
297 except Exception, e: # pylint: disable=broad-except
298 self._debug('Failed to init SAML2 provider: %r' % e)
301 self._root.logout.add_handler(self.name, self.idp_initiated_logout)
303 # Import all known applications
304 data = self.get_data()
307 if 'type' not in sp or sp['type'] != 'SP':
309 if 'name' not in sp or 'metadata' not in sp:
313 except Exception, e: # pylint: disable=broad-except
314 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
319 super(IdpProvider, self).on_enable()
320 self.idp = self.init_idp()
321 if hasattr(self, 'admin'):
325 def idp_initiated_logout(self):
327 Logout all SP sessions when the logout comes from the IdP.
329 For the current user only.
331 self._debug("IdP-initiated SAML2 logout")
334 saml_sessions = us.get_provider_data('saml2')
335 if saml_sessions is None:
336 self._debug("No SAML2 sessions to logout")
338 session = saml_sessions.get_next_logout(remove=False)
342 # Add a fake session to indicate where the user should
343 # be redirected to when all SP's are logged out.
344 idpurl = self._root.instance_base_url()
345 saml_sessions.add_session("_idp_initiated_logout",
348 init_session = saml_sessions.find_session_by_provider(idpurl)
349 init_session.set_logoutstate(idpurl, "idp_initiated_logout", None)
350 saml_sessions.start_logout(init_session)
352 logout = self.idp.get_logout_handler()
353 logout.setSessionFromDump(session.session.dump())
354 logout.initRequest(session.provider_id)
356 logout.buildRequestMsg()
357 except lasso.Error, e:
358 self.error('failure to build logout request msg: %s' % e)
359 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
362 raise cherrypy.HTTPRedirect(logout.msgUrl)
365 class IdpMetadataGenerator(object):
367 def __init__(self, url, idp_cert, expiration=None):
368 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
369 self.meta.set_entity_id('%s/saml2/metadata' % url)
370 self.meta.add_certs(idp_cert, idp_cert)
371 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
372 '%s/saml2/SSO/POST' % url)
373 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
374 '%s/saml2/SSO/Redirect' % url)
375 self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
376 '%s/saml2/SLO/Redirect' % url)
377 self.meta.add_allowed_name_format(
378 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
379 self.meta.add_allowed_name_format(
380 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
381 self.meta.add_allowed_name_format(
382 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
384 def output(self, path=None):
385 return self.meta.output(path)
388 class Installer(ProviderInstaller):
390 def __init__(self, *pargs):
391 super(Installer, self).__init__()
395 def install_args(self, group):
396 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
397 help='Configure SAML2 Provider')
399 def configure(self, opts):
400 if opts['saml2'] != 'yes':
403 # Check storage path is present or create it
404 path = os.path.join(opts['data_dir'], 'saml2')
405 if not os.path.exists(path):
406 os.makedirs(path, 0700)
408 # Use the same cert for signing and ecnryption for now
409 cert = Certificate(path)
410 cert.generate('idp', opts['hostname'])
412 # Generate Idp Metadata
414 if opts['secure'].lower() == 'no':
416 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
417 meta = IdpMetadataGenerator(url, cert,
418 timedelta(METADATA_VALIDITY_PERIOD))
419 if 'krb' in opts and opts['krb'] == 'yes':
420 meta.meta.add_allowed_name_format(
421 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
423 meta.output(os.path.join(path, 'metadata.xml'))
425 # Add configuration data to database
426 po = PluginObject(*self.pargs)
429 po.wipe_config_values()
430 config = {'idp storage path': path,
431 'idp metadata file': 'metadata.xml',
432 'idp certificate file': cert.cert,
433 'idp key file': cert.key,
434 'idp nameid salt': uuid.uuid4().hex}
435 po.save_plugin_config(config)
437 # Update global config to add login plugin
439 po.save_enabled_state()
441 # Fixup permissions so only the ipsilon user can read these files
442 files.fix_user_dirs(path, opts['system_user'])