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
19 from ipsilon.providers.saml2.auth import AuthenticateRequest
20 from ipsilon.providers.saml2.logout import LogoutRequest
21 from ipsilon.providers.saml2.admin import Saml2AdminPage
22 from ipsilon.providers.saml2.provider import IdentityProvider
23 from ipsilon.tools.certs import Certificate
24 from ipsilon.tools import saml2metadata as metadata
25 from ipsilon.tools import files
26 from ipsilon.util.user import UserSession
27 from ipsilon.util.plugin import PluginObject
28 from ipsilon.util import config as pconfig
30 from datetime import timedelta
36 class Redirect(AuthenticateRequest):
38 def GET(self, *args, **kwargs):
40 query = cherrypy.request.query_string
42 login = self.saml2login(query)
43 return self.auth(login)
46 class POSTAuth(AuthenticateRequest):
48 def POST(self, *args, **kwargs):
50 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
51 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
53 login = self.saml2login(request)
54 login.set_msgRelayState(relaystate)
55 return self.auth(login)
58 class Continue(AuthenticateRequest):
60 def GET(self, *args, **kwargs):
62 session = UserSession()
63 user = session.get_user()
64 transdata = self.trans.retrieve()
65 self.stage = transdata['saml2_stage']
68 self._debug("User is marked anonymous?!")
69 # TODO: Return to SP with auth failed error
70 raise cherrypy.HTTPError(401)
72 self._debug('Continue auth for %s' % user.name)
74 if 'saml2_request' not in transdata:
75 self._debug("Couldn't find Request dump?!")
76 # TODO: Return to SP with auth failed error
77 raise cherrypy.HTTPError(400)
78 dump = transdata['saml2_request']
81 login = self.cfg.idp.get_login_handler(dump)
82 except Exception, e: # pylint: disable=broad-except
83 self._debug('Failed to load status from dump: %r' % e)
86 self._debug("Empty Request dump?!")
87 # TODO: Return to SP with auth failed error
88 raise cherrypy.HTTPError(400)
90 return self.auth(login)
93 class RedirectLogout(LogoutRequest):
95 def GET(self, *args, **kwargs):
96 query = cherrypy.request.query_string
98 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
99 response = kwargs.get(lasso.SAML2_FIELD_RESPONSE)
101 return self.logout(query,
102 relaystate=relaystate,
103 samlresponse=response)
106 class SSO(ProviderPageBase):
108 def __init__(self, *args, **kwargs):
109 super(SSO, self).__init__(*args, **kwargs)
110 self.Redirect = Redirect(*args, **kwargs)
111 self.POST = POSTAuth(*args, **kwargs)
112 self.Continue = Continue(*args, **kwargs)
115 class SLO(ProviderPageBase):
117 def __init__(self, *args, **kwargs):
118 super(SLO, self).__init__(*args, **kwargs)
119 self._debug('SLO init')
120 self.Redirect = RedirectLogout(*args, **kwargs)
124 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
126 METADATA_VALIDITY_PERIOD = 30
129 class Metadata(ProviderPageBase):
130 def GET(self, *args, **kwargs):
132 body = self._get_metadata()
133 cherrypy.response.headers["Content-Type"] = "text/xml"
134 cherrypy.response.headers["Content-Disposition"] = \
135 'attachment; filename="metadata.xml"'
138 def _get_metadata(self):
139 if os.path.isfile(self.cfg.idp_metadata_file):
140 s = os.stat(self.cfg.idp_metadata_file)
141 if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
142 with open(self.cfg.idp_metadata_file) as m:
145 # Otherwise generate and save
146 idp_cert = Certificate()
147 idp_cert.import_cert(self.cfg.idp_certificate_file,
148 self.cfg.idp_key_file)
149 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
150 timedelta(METADATA_VALIDITY_PERIOD))
152 with open(self.cfg.idp_metadata_file, 'w+') as m:
157 class SAML2(ProviderPageBase):
159 def __init__(self, *args, **kwargs):
160 super(SAML2, self).__init__(*args, **kwargs)
161 self.metadata = Metadata(*args, **kwargs)
162 self.SSO = SSO(*args, **kwargs)
163 self.SLO = SLO(*args, **kwargs)
166 class IdpProvider(ProviderBase):
168 def __init__(self, *pargs):
169 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
173 self.description = """
174 Provides SAML 2.0 authentication infrastructure. """
180 'Path to data storage accessible by the IdP.',
181 '/var/lib/ipsilon/saml2'),
184 'The IdP Metadata file genearated at install time.',
187 'idp certificate file',
188 'The IdP PEM Certificate genearated at install time.',
192 'The IdP Certificate Key genearated at install time.',
195 'allow self registration',
196 'Allow authenticated users to register applications.',
199 'default allowed nameids',
200 'Default Allowed NameIDs for Service Providers.',
201 metadata.SAML2_NAMEID_MAP.keys(),
202 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
205 'Default NameID used by Service Providers.',
206 metadata.SAML2_NAMEID_MAP.keys(),
209 'default email domain',
210 'Used for users missing the email property.',
213 'default attribute mapping',
214 'Defines how to map attributes before returning them to SPs',
217 'default allowed attributes',
218 'Defines a list of allowed attributes, applied after mapping',
221 if cherrypy.config.get('debug', False):
224 logger = logging.getLogger('lasso')
225 lh = logging.StreamHandler(sys.stderr)
226 logger.addHandler(lh)
227 logger.setLevel(logging.DEBUG)
230 def allow_self_registration(self):
231 return self.get_config_value('allow self registration')
234 def idp_storage_path(self):
235 return self.get_config_value('idp storage path')
238 def idp_metadata_file(self):
239 return os.path.join(self.idp_storage_path,
240 self.get_config_value('idp metadata file'))
243 def idp_certificate_file(self):
244 return os.path.join(self.idp_storage_path,
245 self.get_config_value('idp certificate file'))
248 def idp_key_file(self):
249 return os.path.join(self.idp_storage_path,
250 self.get_config_value('idp key file'))
253 def default_allowed_nameids(self):
254 return self.get_config_value('default allowed nameids')
257 def default_nameid(self):
258 return self.get_config_value('default nameid')
261 def default_email_domain(self):
262 return self.get_config_value('default email domain')
265 def default_attribute_mapping(self):
266 return self.get_config_value('default attribute mapping')
269 def default_allowed_attributes(self):
270 return self.get_config_value('default allowed attributes')
272 def get_tree(self, site):
273 self.idp = self.init_idp()
274 self.page = SAML2(site, self)
275 self.admin = Saml2AdminPage(site, self)
282 idp = IdentityProvider(self)
283 except Exception, e: # pylint: disable=broad-except
284 self._debug('Failed to init SAML2 provider: %r' % e)
287 # Import all known applications
288 data = self.get_data()
291 if 'type' not in sp or sp['type'] != 'SP':
293 if 'name' not in sp or 'metadata' not in sp:
297 except Exception, e: # pylint: disable=broad-except
298 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
303 super(IdpProvider, self).on_enable()
304 self.idp = self.init_idp()
305 if hasattr(self, 'admin'):
310 class IdpMetadataGenerator(object):
312 def __init__(self, url, idp_cert, expiration=None):
313 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
314 self.meta.set_entity_id('%s/saml2/metadata' % url)
315 self.meta.add_certs(idp_cert, idp_cert)
316 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
317 '%s/saml2/SSO/POST' % url)
318 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
319 '%s/saml2/SSO/Redirect' % url)
320 self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
321 '%s/saml2/SLO/Redirect' % url)
322 self.meta.add_allowed_name_format(
323 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
324 self.meta.add_allowed_name_format(
325 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
326 self.meta.add_allowed_name_format(
327 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
329 def output(self, path=None):
330 return self.meta.output(path)
333 class Installer(object):
335 def __init__(self, *pargs):
337 self.ptype = 'provider'
340 def install_args(self, group):
341 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
342 help='Configure SAML2 Provider')
344 def configure(self, opts):
345 if opts['saml2'] != 'yes':
348 # Check storage path is present or create it
349 path = os.path.join(opts['data_dir'], 'saml2')
350 if not os.path.exists(path):
351 os.makedirs(path, 0700)
353 # Use the same cert for signing and ecnryption for now
354 cert = Certificate(path)
355 cert.generate('idp', opts['hostname'])
357 # Generate Idp Metadata
359 if opts['secure'].lower() == 'no':
361 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
362 meta = IdpMetadataGenerator(url, cert,
363 timedelta(METADATA_VALIDITY_PERIOD))
364 if 'krb' in opts and opts['krb'] == 'yes':
365 meta.meta.add_allowed_name_format(
366 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
368 meta.output(os.path.join(path, 'metadata.xml'))
370 # Add configuration data to database
371 po = PluginObject(*self.pargs)
374 po.wipe_config_values()
375 config = {'idp storage path': path,
376 'idp metadata file': 'metadata.xml',
377 'idp certificate file': cert.cert,
378 'idp key file': cert.key}
379 po.save_plugin_config(config)
381 # Update global config to add login plugin
383 po.save_enabled_state()
385 # Fixup permissions so only the ipsilon user can read these files
386 files.fix_user_dirs(path, opts['system_user'])