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
128 # five years (approximately)
129 METADATA_DEFAULT_VALIDITY_PERIOD = 365 * 5
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)
153 validity = int(self.cfg.idp_metadata_validity)
154 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
157 with open(self.cfg.idp_metadata_file, 'w+') as m:
162 class SAML2(ProviderPageBase):
164 def __init__(self, *args, **kwargs):
165 super(SAML2, self).__init__(*args, **kwargs)
166 self.metadata = Metadata(*args, **kwargs)
167 self.SSO = SSO(*args, **kwargs)
168 self.SLO = SLO(*args, **kwargs)
171 class IdpProvider(ProviderBase):
173 def __init__(self, *pargs):
174 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
179 self.description = """
180 Provides SAML 2.0 authentication infrastructure. """
186 'Path to data storage accessible by the IdP.',
187 '/var/lib/ipsilon/saml2'),
190 'The IdP Metadata file generated at install time.',
193 'idp metadata validity',
194 'The IdP Metadata validity period (in days) to use when '
195 'generating new metadata.',
196 METADATA_DEFAULT_VALIDITY_PERIOD),
198 'idp certificate file',
199 'The IdP PEM Certificate generated at install time.',
203 'The IdP Certificate Key generated at install time.',
207 'The salt used for persistent Name IDs.',
210 'allow self registration',
211 'Allow authenticated users to register applications.',
214 'default allowed nameids',
215 'Default Allowed NameIDs for Service Providers.',
216 metadata.SAML2_NAMEID_MAP.keys(),
217 ['unspecified', 'persistent', 'transient', 'email',
218 'kerberos', 'x509']),
221 'Default NameID used by Service Providers.',
222 metadata.SAML2_NAMEID_MAP.keys(),
225 'default email domain',
226 'Used for users missing the email property.',
229 'default attribute mapping',
230 'Defines how to map attributes before returning them to SPs',
233 'default allowed attributes',
234 'Defines a list of allowed attributes, applied after mapping',
237 if cherrypy.config.get('debug', False):
240 logger = logging.getLogger('lasso')
241 lh = logging.StreamHandler(sys.stderr)
242 logger.addHandler(lh)
243 logger.setLevel(logging.DEBUG)
246 def allow_self_registration(self):
247 return self.get_config_value('allow self registration')
250 def idp_storage_path(self):
251 return self.get_config_value('idp storage path')
254 def idp_metadata_file(self):
255 return os.path.join(self.idp_storage_path,
256 self.get_config_value('idp metadata file'))
259 def idp_metadata_validity(self):
260 return self.get_config_value('idp metadata validity')
263 def idp_certificate_file(self):
264 return os.path.join(self.idp_storage_path,
265 self.get_config_value('idp certificate file'))
268 def idp_key_file(self):
269 return os.path.join(self.idp_storage_path,
270 self.get_config_value('idp key file'))
273 def idp_nameid_salt(self):
274 return self.get_config_value('idp nameid salt')
277 def default_allowed_nameids(self):
278 return self.get_config_value('default allowed nameids')
281 def default_nameid(self):
282 return self.get_config_value('default nameid')
285 def default_email_domain(self):
286 return self.get_config_value('default email domain')
289 def default_attribute_mapping(self):
290 return self.get_config_value('default attribute mapping')
293 def default_allowed_attributes(self):
294 return self.get_config_value('default allowed attributes')
296 def get_tree(self, site):
297 self.idp = self.init_idp()
298 self.page = SAML2(site, self)
299 self.admin = Saml2AdminPage(site, self)
300 self.rest = Saml2RestBase(site, self)
307 idp = IdentityProvider(self)
308 except Exception, e: # pylint: disable=broad-except
309 self.debug('Failed to init SAML2 provider: %r' % e)
312 self._root.logout.add_handler(self.name, self.idp_initiated_logout)
314 # Import all known applications
315 data = self.get_data()
318 if 'type' not in sp or sp['type'] != 'SP':
320 if 'name' not in sp or 'metadata' not in sp:
324 except Exception, e: # pylint: disable=broad-except
325 self.debug('Failed to add SP %s: %r' % (sp['name'], e))
330 super(IdpProvider, self).on_enable()
331 self.idp = self.init_idp()
332 if hasattr(self, 'admin'):
336 def idp_initiated_logout(self):
338 Logout all SP sessions when the logout comes from the IdP.
340 For the current user only.
342 self.debug("IdP-initiated SAML2 logout")
345 saml_sessions = us.get_provider_data('saml2')
346 if saml_sessions is None:
347 self.debug("No SAML2 sessions to logout")
349 session = saml_sessions.get_next_logout(remove=False)
353 # Add a fake session to indicate where the user should
354 # be redirected to when all SP's are logged out.
355 idpurl = self._root.instance_base_url()
356 saml_sessions.add_session("_idp_initiated_logout",
359 init_session = saml_sessions.find_session_by_provider(idpurl)
360 init_session.set_logoutstate(idpurl, "idp_initiated_logout", None)
361 saml_sessions.start_logout(init_session)
363 logout = self.idp.get_logout_handler()
364 logout.setSessionFromDump(session.session.dump())
365 logout.initRequest(session.provider_id)
367 logout.buildRequestMsg()
368 except lasso.Error, e:
369 self.error('failure to build logout request msg: %s' % e)
370 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
373 raise cherrypy.HTTPRedirect(logout.msgUrl)
376 class IdpMetadataGenerator(object):
378 def __init__(self, url, idp_cert, expiration=None):
379 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
380 self.meta.set_entity_id('%s/saml2/metadata' % url)
381 self.meta.add_certs(idp_cert, idp_cert)
382 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
383 '%s/saml2/SSO/POST' % url)
384 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
385 '%s/saml2/SSO/Redirect' % url)
386 self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
387 '%s/saml2/SLO/Redirect' % url)
388 self.meta.add_allowed_name_format(
389 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
390 self.meta.add_allowed_name_format(
391 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
392 self.meta.add_allowed_name_format(
393 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
395 def output(self, path=None):
396 return self.meta.output(path)
399 class Installer(ProviderInstaller):
401 def __init__(self, *pargs):
402 super(Installer, self).__init__()
406 def install_args(self, group):
407 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
408 help='Configure SAML2 Provider')
409 group.add_argument('--saml2-metadata-validity',
410 default=METADATA_DEFAULT_VALIDITY_PERIOD,
411 help=('Metadata validity period in days '
413 METADATA_DEFAULT_VALIDITY_PERIOD))
415 def configure(self, opts):
416 if opts['saml2'] != 'yes':
419 # Check storage path is present or create it
420 path = os.path.join(opts['data_dir'], 'saml2')
421 if not os.path.exists(path):
422 os.makedirs(path, 0700)
424 # Use the same cert for signing and ecnryption for now
425 cert = Certificate(path)
426 cert.generate('idp', opts['hostname'])
428 # Generate Idp Metadata
430 if opts['secure'].lower() == 'no':
432 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
433 validity = int(opts['saml2_metadata_validity'])
434 meta = IdpMetadataGenerator(url, cert,
436 if 'gssapi' in opts and opts['gssapi'] == 'yes':
437 meta.meta.add_allowed_name_format(
438 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
440 meta.output(os.path.join(path, 'metadata.xml'))
442 # Add configuration data to database
443 po = PluginObject(*self.pargs)
446 po.wipe_config_values()
447 config = {'idp storage path': path,
448 'idp metadata file': 'metadata.xml',
449 'idp certificate file': cert.cert,
450 'idp key file': cert.key,
451 'idp nameid salt': uuid.uuid4().hex,
452 'idp metadata validity': opts['saml2_metadata_validity']}
453 po.save_plugin_config(config)
455 # Update global config to add login plugin
457 po.save_enabled_state()
459 # Fixup permissions so only the ipsilon user can read these files
460 files.fix_user_dirs(path, opts['system_user'])