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.provider import IdentityProvider
24 from ipsilon.tools.certs import Certificate
25 from ipsilon.tools import saml2metadata as metadata
26 from ipsilon.tools import files
27 from ipsilon.util.user import UserSession
28 from ipsilon.util.plugin import PluginObject
29 from ipsilon.util import config as pconfig
31 from datetime import timedelta
37 class Redirect(AuthenticateRequest):
39 def GET(self, *args, **kwargs):
41 query = cherrypy.request.query_string
43 login = self.saml2login(query)
44 return self.auth(login)
47 class POSTAuth(AuthenticateRequest):
49 def POST(self, *args, **kwargs):
51 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
52 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
54 login = self.saml2login(request)
55 login.set_msgRelayState(relaystate)
56 return self.auth(login)
59 class Continue(AuthenticateRequest):
61 def GET(self, *args, **kwargs):
63 session = UserSession()
64 user = session.get_user()
65 transdata = self.trans.retrieve()
66 self.stage = transdata['saml2_stage']
69 self._debug("User is marked anonymous?!")
70 # TODO: Return to SP with auth failed error
71 raise cherrypy.HTTPError(401)
73 self._debug('Continue auth for %s' % user.name)
75 if 'saml2_request' not in transdata:
76 self._debug("Couldn't find Request dump?!")
77 # TODO: Return to SP with auth failed error
78 raise cherrypy.HTTPError(400)
79 dump = transdata['saml2_request']
82 login = self.cfg.idp.get_login_handler(dump)
83 except Exception, e: # pylint: disable=broad-except
84 self._debug('Failed to load status from dump: %r' % e)
87 self._debug("Empty Request dump?!")
88 # TODO: Return to SP with auth failed error
89 raise cherrypy.HTTPError(400)
91 return self.auth(login)
94 class RedirectLogout(LogoutRequest):
96 def GET(self, *args, **kwargs):
97 query = cherrypy.request.query_string
99 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
100 response = kwargs.get(lasso.SAML2_FIELD_RESPONSE)
102 return self.logout(query,
103 relaystate=relaystate,
104 samlresponse=response)
107 class SSO(ProviderPageBase):
109 def __init__(self, *args, **kwargs):
110 super(SSO, self).__init__(*args, **kwargs)
111 self.Redirect = Redirect(*args, **kwargs)
112 self.POST = POSTAuth(*args, **kwargs)
113 self.Continue = Continue(*args, **kwargs)
116 class SLO(ProviderPageBase):
118 def __init__(self, *args, **kwargs):
119 super(SLO, self).__init__(*args, **kwargs)
120 self._debug('SLO init')
121 self.Redirect = RedirectLogout(*args, **kwargs)
125 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
127 METADATA_VALIDITY_PERIOD = 30
130 class Metadata(ProviderPageBase):
131 def GET(self, *args, **kwargs):
133 body = self._get_metadata()
134 cherrypy.response.headers["Content-Type"] = "text/xml"
135 cherrypy.response.headers["Content-Disposition"] = \
136 'attachment; filename="metadata.xml"'
139 def _get_metadata(self):
140 if os.path.isfile(self.cfg.idp_metadata_file):
141 s = os.stat(self.cfg.idp_metadata_file)
142 if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
143 with open(self.cfg.idp_metadata_file) as m:
146 # Otherwise generate and save
147 idp_cert = Certificate()
148 idp_cert.import_cert(self.cfg.idp_certificate_file,
149 self.cfg.idp_key_file)
150 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
151 timedelta(METADATA_VALIDITY_PERIOD))
153 with open(self.cfg.idp_metadata_file, 'w+') as m:
158 class SAML2(ProviderPageBase):
160 def __init__(self, *args, **kwargs):
161 super(SAML2, self).__init__(*args, **kwargs)
162 self.metadata = Metadata(*args, **kwargs)
163 self.SSO = SSO(*args, **kwargs)
164 self.SLO = SLO(*args, **kwargs)
167 class IdpProvider(ProviderBase):
169 def __init__(self, *pargs):
170 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
174 self.description = """
175 Provides SAML 2.0 authentication infrastructure. """
181 'Path to data storage accessible by the IdP.',
182 '/var/lib/ipsilon/saml2'),
185 'The IdP Metadata file genearated at install time.',
188 'idp certificate file',
189 'The IdP PEM Certificate genearated at install time.',
193 'The IdP Certificate Key genearated at install time.',
196 'allow self registration',
197 'Allow authenticated users to register applications.',
200 'default allowed nameids',
201 'Default Allowed NameIDs for Service Providers.',
202 metadata.SAML2_NAMEID_MAP.keys(),
203 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
206 'Default NameID used by Service Providers.',
207 metadata.SAML2_NAMEID_MAP.keys(),
210 'default email domain',
211 'Used for users missing the email property.',
214 'default attribute mapping',
215 'Defines how to map attributes before returning them to SPs',
218 'default allowed attributes',
219 'Defines a list of allowed attributes, applied after mapping',
222 if cherrypy.config.get('debug', False):
225 logger = logging.getLogger('lasso')
226 lh = logging.StreamHandler(sys.stderr)
227 logger.addHandler(lh)
228 logger.setLevel(logging.DEBUG)
231 def allow_self_registration(self):
232 return self.get_config_value('allow self registration')
235 def idp_storage_path(self):
236 return self.get_config_value('idp storage path')
239 def idp_metadata_file(self):
240 return os.path.join(self.idp_storage_path,
241 self.get_config_value('idp metadata file'))
244 def idp_certificate_file(self):
245 return os.path.join(self.idp_storage_path,
246 self.get_config_value('idp certificate file'))
249 def idp_key_file(self):
250 return os.path.join(self.idp_storage_path,
251 self.get_config_value('idp key file'))
254 def default_allowed_nameids(self):
255 return self.get_config_value('default allowed nameids')
258 def default_nameid(self):
259 return self.get_config_value('default nameid')
262 def default_email_domain(self):
263 return self.get_config_value('default email domain')
266 def default_attribute_mapping(self):
267 return self.get_config_value('default attribute mapping')
270 def default_allowed_attributes(self):
271 return self.get_config_value('default allowed attributes')
273 def get_tree(self, site):
274 self.idp = self.init_idp()
275 self.page = SAML2(site, self)
276 self.admin = Saml2AdminPage(site, self)
283 idp = IdentityProvider(self)
284 except Exception, e: # pylint: disable=broad-except
285 self._debug('Failed to init SAML2 provider: %r' % e)
288 # Import all known applications
289 data = self.get_data()
292 if 'type' not in sp or sp['type'] != 'SP':
294 if 'name' not in sp or 'metadata' not in sp:
298 except Exception, e: # pylint: disable=broad-except
299 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
304 super(IdpProvider, self).on_enable()
305 self.idp = self.init_idp()
306 if hasattr(self, 'admin'):
311 class IdpMetadataGenerator(object):
313 def __init__(self, url, idp_cert, expiration=None):
314 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
315 self.meta.set_entity_id('%s/saml2/metadata' % url)
316 self.meta.add_certs(idp_cert, idp_cert)
317 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
318 '%s/saml2/SSO/POST' % url)
319 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
320 '%s/saml2/SSO/Redirect' % url)
321 self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
322 '%s/saml2/SLO/Redirect' % url)
323 self.meta.add_allowed_name_format(
324 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
325 self.meta.add_allowed_name_format(
326 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
327 self.meta.add_allowed_name_format(
328 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
330 def output(self, path=None):
331 return self.meta.output(path)
334 class Installer(ProviderInstaller):
336 def __init__(self, *pargs):
337 super(Installer, self).__init__()
341 def install_args(self, group):
342 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
343 help='Configure SAML2 Provider')
345 def configure(self, opts):
346 if opts['saml2'] != 'yes':
349 # Check storage path is present or create it
350 path = os.path.join(opts['data_dir'], 'saml2')
351 if not os.path.exists(path):
352 os.makedirs(path, 0700)
354 # Use the same cert for signing and ecnryption for now
355 cert = Certificate(path)
356 cert.generate('idp', opts['hostname'])
358 # Generate Idp Metadata
360 if opts['secure'].lower() == 'no':
362 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
363 meta = IdpMetadataGenerator(url, cert,
364 timedelta(METADATA_VALIDITY_PERIOD))
365 if 'krb' in opts and opts['krb'] == 'yes':
366 meta.meta.add_allowed_name_format(
367 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
369 meta.output(os.path.join(path, 'metadata.xml'))
371 # Add configuration data to database
372 po = PluginObject(*self.pargs)
375 po.wipe_config_values()
376 config = {'idp storage path': path,
377 'idp metadata file': 'metadata.xml',
378 'idp certificate file': cert.cert,
379 'idp key file': cert.key}
380 po.save_plugin_config(config)
382 # Update global config to add login plugin
384 po.save_enabled_state()
386 # Fixup permissions so only the ipsilon user can read these files
387 files.fix_user_dirs(path, opts['system_user'])