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
38 class Redirect(AuthenticateRequest):
40 def GET(self, *args, **kwargs):
42 query = cherrypy.request.query_string
44 login = self.saml2login(query)
45 return self.auth(login)
48 class POSTAuth(AuthenticateRequest):
50 def POST(self, *args, **kwargs):
52 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
53 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
55 login = self.saml2login(request)
56 login.set_msgRelayState(relaystate)
57 return self.auth(login)
60 class Continue(AuthenticateRequest):
62 def GET(self, *args, **kwargs):
64 session = UserSession()
65 user = session.get_user()
66 transdata = self.trans.retrieve()
67 self.stage = transdata['saml2_stage']
70 self._debug("User is marked anonymous?!")
71 # TODO: Return to SP with auth failed error
72 raise cherrypy.HTTPError(401)
74 self._debug('Continue auth for %s' % user.name)
76 if 'saml2_request' not in transdata:
77 self._debug("Couldn't find Request dump?!")
78 # TODO: Return to SP with auth failed error
79 raise cherrypy.HTTPError(400)
80 dump = transdata['saml2_request']
83 login = self.cfg.idp.get_login_handler(dump)
84 except Exception, e: # pylint: disable=broad-except
85 self._debug('Failed to load status from dump: %r' % e)
88 self._debug("Empty Request dump?!")
89 # TODO: Return to SP with auth failed error
90 raise cherrypy.HTTPError(400)
92 return self.auth(login)
95 class RedirectLogout(LogoutRequest):
97 def GET(self, *args, **kwargs):
98 query = cherrypy.request.query_string
100 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
101 response = kwargs.get(lasso.SAML2_FIELD_RESPONSE)
103 return self.logout(query,
104 relaystate=relaystate,
105 samlresponse=response)
108 class SSO(ProviderPageBase):
110 def __init__(self, *args, **kwargs):
111 super(SSO, self).__init__(*args, **kwargs)
112 self.Redirect = Redirect(*args, **kwargs)
113 self.POST = POSTAuth(*args, **kwargs)
114 self.Continue = Continue(*args, **kwargs)
117 class SLO(ProviderPageBase):
119 def __init__(self, *args, **kwargs):
120 super(SLO, self).__init__(*args, **kwargs)
121 self._debug('SLO init')
122 self.Redirect = RedirectLogout(*args, **kwargs)
126 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
128 METADATA_VALIDITY_PERIOD = 30
131 class Metadata(ProviderPageBase):
132 def GET(self, *args, **kwargs):
134 body = self._get_metadata()
135 cherrypy.response.headers["Content-Type"] = "text/xml"
136 cherrypy.response.headers["Content-Disposition"] = \
137 'attachment; filename="metadata.xml"'
140 def _get_metadata(self):
141 if os.path.isfile(self.cfg.idp_metadata_file):
142 s = os.stat(self.cfg.idp_metadata_file)
143 if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
144 with open(self.cfg.idp_metadata_file) as m:
147 # Otherwise generate and save
148 idp_cert = Certificate()
149 idp_cert.import_cert(self.cfg.idp_certificate_file,
150 self.cfg.idp_key_file)
151 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
152 timedelta(METADATA_VALIDITY_PERIOD))
154 with open(self.cfg.idp_metadata_file, 'w+') as m:
159 class SAML2(ProviderPageBase):
161 def __init__(self, *args, **kwargs):
162 super(SAML2, self).__init__(*args, **kwargs)
163 self.metadata = Metadata(*args, **kwargs)
164 self.SSO = SSO(*args, **kwargs)
165 self.SLO = SLO(*args, **kwargs)
168 class IdpProvider(ProviderBase):
170 def __init__(self, *pargs):
171 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
176 self.description = """
177 Provides SAML 2.0 authentication infrastructure. """
183 'Path to data storage accessible by the IdP.',
184 '/var/lib/ipsilon/saml2'),
187 'The IdP Metadata file genearated at install time.',
190 'idp certificate file',
191 'The IdP PEM Certificate genearated at install time.',
195 'The IdP Certificate Key genearated at install time.',
198 'allow self registration',
199 'Allow authenticated users to register applications.',
202 'default allowed nameids',
203 'Default Allowed NameIDs for Service Providers.',
204 metadata.SAML2_NAMEID_MAP.keys(),
205 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
208 'Default NameID used by Service Providers.',
209 metadata.SAML2_NAMEID_MAP.keys(),
212 'default email domain',
213 'Used for users missing the email property.',
216 'default attribute mapping',
217 'Defines how to map attributes before returning them to SPs',
220 'default allowed attributes',
221 'Defines a list of allowed attributes, applied after mapping',
224 if cherrypy.config.get('debug', False):
227 logger = logging.getLogger('lasso')
228 lh = logging.StreamHandler(sys.stderr)
229 logger.addHandler(lh)
230 logger.setLevel(logging.DEBUG)
233 def allow_self_registration(self):
234 return self.get_config_value('allow self registration')
237 def idp_storage_path(self):
238 return self.get_config_value('idp storage path')
241 def idp_metadata_file(self):
242 return os.path.join(self.idp_storage_path,
243 self.get_config_value('idp metadata file'))
246 def idp_certificate_file(self):
247 return os.path.join(self.idp_storage_path,
248 self.get_config_value('idp certificate file'))
251 def idp_key_file(self):
252 return os.path.join(self.idp_storage_path,
253 self.get_config_value('idp key file'))
256 def default_allowed_nameids(self):
257 return self.get_config_value('default allowed nameids')
260 def default_nameid(self):
261 return self.get_config_value('default nameid')
264 def default_email_domain(self):
265 return self.get_config_value('default email domain')
268 def default_attribute_mapping(self):
269 return self.get_config_value('default attribute mapping')
272 def default_allowed_attributes(self):
273 return self.get_config_value('default allowed attributes')
275 def get_tree(self, site):
276 self.idp = self.init_idp()
277 self.page = SAML2(site, self)
278 self.admin = Saml2AdminPage(site, self)
279 self.rest = Saml2RestBase(site, self)
286 idp = IdentityProvider(self)
287 except Exception, e: # pylint: disable=broad-except
288 self._debug('Failed to init SAML2 provider: %r' % e)
291 # Import all known applications
292 data = self.get_data()
295 if 'type' not in sp or sp['type'] != 'SP':
297 if 'name' not in sp or 'metadata' not in sp:
301 except Exception, e: # pylint: disable=broad-except
302 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
307 super(IdpProvider, self).on_enable()
308 self.idp = self.init_idp()
309 if hasattr(self, 'admin'):
314 class IdpMetadataGenerator(object):
316 def __init__(self, url, idp_cert, expiration=None):
317 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
318 self.meta.set_entity_id('%s/saml2/metadata' % url)
319 self.meta.add_certs(idp_cert, idp_cert)
320 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
321 '%s/saml2/SSO/POST' % url)
322 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
323 '%s/saml2/SSO/Redirect' % url)
324 self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
325 '%s/saml2/SLO/Redirect' % url)
326 self.meta.add_allowed_name_format(
327 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
328 self.meta.add_allowed_name_format(
329 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
330 self.meta.add_allowed_name_format(
331 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
333 def output(self, path=None):
334 return self.meta.output(path)
337 class Installer(ProviderInstaller):
339 def __init__(self, *pargs):
340 super(Installer, self).__init__()
344 def install_args(self, group):
345 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
346 help='Configure SAML2 Provider')
348 def configure(self, opts):
349 if opts['saml2'] != 'yes':
352 # Check storage path is present or create it
353 path = os.path.join(opts['data_dir'], 'saml2')
354 if not os.path.exists(path):
355 os.makedirs(path, 0700)
357 # Use the same cert for signing and ecnryption for now
358 cert = Certificate(path)
359 cert.generate('idp', opts['hostname'])
361 # Generate Idp Metadata
363 if opts['secure'].lower() == 'no':
365 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
366 meta = IdpMetadataGenerator(url, cert,
367 timedelta(METADATA_VALIDITY_PERIOD))
368 if 'krb' in opts and opts['krb'] == 'yes':
369 meta.meta.add_allowed_name_format(
370 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
372 meta.output(os.path.join(path, 'metadata.xml'))
374 # Add configuration data to database
375 po = PluginObject(*self.pargs)
378 po.wipe_config_values()
379 config = {'idp storage path': path,
380 'idp metadata file': 'metadata.xml',
381 'idp certificate file': cert.cert,
382 'idp key file': cert.key}
383 po.save_plugin_config(config)
385 # Update global config to add login plugin
387 po.save_enabled_state()
389 # Fixup permissions so only the ipsilon user can read these files
390 files.fix_user_dirs(path, opts['system_user'])