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.admin import Saml2AdminPage
21 from ipsilon.providers.saml2.provider import IdentityProvider
22 from ipsilon.tools.certs import Certificate
23 from ipsilon.tools import saml2metadata as metadata
24 from ipsilon.tools import files
25 from ipsilon.util.user import UserSession
26 from ipsilon.util.plugin import PluginObject
27 from ipsilon.util import config as pconfig
29 from datetime import timedelta
35 class Redirect(AuthenticateRequest):
37 def GET(self, *args, **kwargs):
39 query = cherrypy.request.query_string
41 login = self.saml2login(query)
42 return self.auth(login)
45 class POSTAuth(AuthenticateRequest):
47 def POST(self, *args, **kwargs):
49 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
50 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
52 login = self.saml2login(request)
53 login.set_msgRelayState(relaystate)
54 return self.auth(login)
57 class Continue(AuthenticateRequest):
59 def GET(self, *args, **kwargs):
61 session = UserSession()
62 user = session.get_user()
63 transdata = self.trans.retrieve()
64 self.stage = transdata['saml2_stage']
67 self._debug("User is marked anonymous?!")
68 # TODO: Return to SP with auth failed error
69 raise cherrypy.HTTPError(401)
71 self._debug('Continue auth for %s' % user.name)
73 if 'saml2_request' not in transdata:
74 self._debug("Couldn't find Request dump?!")
75 # TODO: Return to SP with auth failed error
76 raise cherrypy.HTTPError(400)
77 dump = transdata['saml2_request']
80 login = self.cfg.idp.get_login_handler(dump)
81 except Exception, e: # pylint: disable=broad-except
82 self._debug('Failed to load status from dump: %r' % e)
85 self._debug("Empty Request dump?!")
86 # TODO: Return to SP with auth failed error
87 raise cherrypy.HTTPError(400)
89 return self.auth(login)
92 class SSO(ProviderPageBase):
94 def __init__(self, *args, **kwargs):
95 super(SSO, self).__init__(*args, **kwargs)
96 self.Redirect = Redirect(*args, **kwargs)
97 self.POST = POSTAuth(*args, **kwargs)
98 self.Continue = Continue(*args, **kwargs)
102 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
104 METADATA_VALIDITY_PERIOD = 30
107 class Metadata(ProviderPageBase):
108 def GET(self, *args, **kwargs):
110 body = self._get_metadata()
111 cherrypy.response.headers["Content-Type"] = "text/xml"
112 cherrypy.response.headers["Content-Disposition"] = \
113 'attachment; filename="metadata.xml"'
116 def _get_metadata(self):
117 if os.path.isfile(self.cfg.idp_metadata_file):
118 s = os.stat(self.cfg.idp_metadata_file)
119 if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
120 with open(self.cfg.idp_metadata_file) as m:
123 # Otherwise generate and save
124 idp_cert = Certificate()
125 idp_cert.import_cert(self.cfg.idp_certificate_file,
126 self.cfg.idp_key_file)
127 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
128 timedelta(METADATA_VALIDITY_PERIOD))
130 with open(self.cfg.idp_metadata_file, 'w+') as m:
135 class SAML2(ProviderPageBase):
137 def __init__(self, *args, **kwargs):
138 super(SAML2, self).__init__(*args, **kwargs)
139 self.metadata = Metadata(*args, **kwargs)
140 self.SSO = SSO(*args, **kwargs)
143 class IdpProvider(ProviderBase):
145 def __init__(self, *pargs):
146 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
150 self.description = """
151 Provides SAML 2.0 authentication infrastructure. """
157 'Path to data storage accessible by the IdP.',
158 '/var/lib/ipsilon/saml2'),
161 'The IdP Metadata file genearated at install time.',
164 'idp certificate file',
165 'The IdP PEM Certificate genearated at install time.',
169 'The IdP Certificate Key genearated at install time.',
172 'allow self registration',
173 'Allow authenticated users to register applications.',
176 'default allowed nameids',
177 'Default Allowed NameIDs for Service Providers.',
178 metadata.SAML2_NAMEID_MAP.keys(),
179 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
182 'Default NameID used by Service Providers.',
183 metadata.SAML2_NAMEID_MAP.keys(),
186 'default email domain',
187 'Used for users missing the email property.',
190 if cherrypy.config.get('debug', False):
193 logger = logging.getLogger('lasso')
194 lh = logging.StreamHandler(sys.stderr)
195 logger.addHandler(lh)
196 logger.setLevel(logging.DEBUG)
199 def allow_self_registration(self):
200 return self.get_config_value('allow self registration')
203 def idp_storage_path(self):
204 return self.get_config_value('idp storage path')
207 def idp_metadata_file(self):
208 return os.path.join(self.idp_storage_path,
209 self.get_config_value('idp metadata file'))
212 def idp_certificate_file(self):
213 return os.path.join(self.idp_storage_path,
214 self.get_config_value('idp certificate file'))
217 def idp_key_file(self):
218 return os.path.join(self.idp_storage_path,
219 self.get_config_value('idp key file'))
222 def default_allowed_nameids(self):
223 return self.get_config_value('default allowed nameids')
226 def default_nameid(self):
227 return self.get_config_value('default nameid')
230 def default_email_domain(self):
231 return self.get_config_value('default email domain')
233 def get_tree(self, site):
234 self.idp = self.init_idp()
235 self.page = SAML2(site, self)
236 self.admin = Saml2AdminPage(site, self)
243 idp = IdentityProvider(self)
244 except Exception, e: # pylint: disable=broad-except
245 self._debug('Failed to init SAML2 provider: %r' % e)
248 # Import all known applications
249 data = self.get_data()
252 if 'type' not in sp or sp['type'] != 'SP':
254 if 'name' not in sp or 'metadata' not in sp:
258 except Exception, e: # pylint: disable=broad-except
259 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
264 super(IdpProvider, self).on_enable()
265 self.idp = self.init_idp()
266 if hasattr(self, 'admin'):
271 class IdpMetadataGenerator(object):
273 def __init__(self, url, idp_cert, expiration=None):
274 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
275 self.meta.set_entity_id('%s/saml2/metadata' % url)
276 self.meta.add_certs(idp_cert, idp_cert)
277 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
278 '%s/saml2/SSO/POST' % url)
279 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
280 '%s/saml2/SSO/Redirect' % url)
281 self.meta.add_allowed_name_format(
282 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
283 self.meta.add_allowed_name_format(
284 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
285 self.meta.add_allowed_name_format(
286 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
288 def output(self, path=None):
289 return self.meta.output(path)
292 class Installer(object):
294 def __init__(self, *pargs):
296 self.ptype = 'provider'
299 def install_args(self, group):
300 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
301 help='Configure SAML2 Provider')
303 def configure(self, opts):
304 if opts['saml2'] != 'yes':
307 # Check storage path is present or create it
308 path = os.path.join(opts['data_dir'], 'saml2')
309 if not os.path.exists(path):
310 os.makedirs(path, 0700)
312 # Use the same cert for signing and ecnryption for now
313 cert = Certificate(path)
314 cert.generate('idp', opts['hostname'])
316 # Generate Idp Metadata
318 if opts['secure'].lower() == 'no':
320 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
321 meta = IdpMetadataGenerator(url, cert,
322 timedelta(METADATA_VALIDITY_PERIOD))
323 if 'krb' in opts and opts['krb'] == 'yes':
324 meta.meta.add_allowed_name_format(
325 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
327 meta.output(os.path.join(path, 'metadata.xml'))
329 # Add configuration data to database
330 po = PluginObject(*self.pargs)
333 po.wipe_config_values()
334 config = {'idp storage path': path,
335 'idp metadata file': 'metadata.xml',
336 'idp certificate file': cert.cert,
337 'idp key file': cert.key}
338 po.save_plugin_config(config)
340 # Update global config to add login plugin
342 po.save_enabled_state()
344 # Fixup permissions so only the ipsilon user can read these files
345 files.fix_user_dirs(path, opts['system_user'])