3 # Copyright (C) 2014 Simo Sorce <simo@redhat.com>
5 # see file 'COPYING' for use and warranty information
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from ipsilon.providers.common import ProviderBase, ProviderPageBase
21 from ipsilon.providers.saml2.auth import AuthenticateRequest
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
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)
101 class Metadata(ProviderPageBase):
102 def GET(self, *args, **kwargs):
103 with open(self.cfg.idp_metadata_file) as m:
105 cherrypy.response.headers["Content-Type"] = "text/xml"
106 cherrypy.response.headers["Content-Disposition"] = \
107 'attachment; filename="metadata.xml"'
111 class SAML2(ProviderPageBase):
113 def __init__(self, *args, **kwargs):
114 super(SAML2, self).__init__(*args, **kwargs)
115 self.metadata = Metadata(*args, **kwargs)
116 self.SSO = SSO(*args, **kwargs)
119 class IdpProvider(ProviderBase):
121 def __init__(self, *pargs):
122 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
126 self.description = """
127 Provides SAML 2.0 authentication infrastructure. """
133 'Path to data storage accessible by the IdP.',
134 '/var/lib/ipsilon/saml2'),
137 'The IdP Metadata file genearated at install time.',
140 'idp certificate file',
141 'The IdP PEM Certificate genearated at install time.',
145 'The IdP Certificate Key genearated at install time.',
148 'allow self registration',
149 'Allow authenticated users to register applications.',
152 'default allowed nameids',
153 'Default Allowed NameIDs for Service Providers.',
154 metadata.SAML2_NAMEID_MAP.keys(),
155 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
158 'Default NameID used by Service Providers.',
159 metadata.SAML2_NAMEID_MAP.keys(),
162 'default email domain',
163 'Used for users missing the email property.',
166 if cherrypy.config.get('debug', False):
169 logger = logging.getLogger('lasso')
170 lh = logging.StreamHandler(sys.stderr)
171 logger.addHandler(lh)
172 logger.setLevel(logging.DEBUG)
175 def allow_self_registration(self):
176 return self.get_config_value('allow self registration')
179 def idp_storage_path(self):
180 return self.get_config_value('idp storage path')
183 def idp_metadata_file(self):
184 return os.path.join(self.idp_storage_path,
185 self.get_config_value('idp metadata file'))
188 def idp_certificate_file(self):
189 return os.path.join(self.idp_storage_path,
190 self.get_config_value('idp certificate file'))
193 def idp_key_file(self):
194 return os.path.join(self.idp_storage_path,
195 self.get_config_value('idp key file'))
198 def default_allowed_nameids(self):
199 return self.get_config_value('default allowed nameids')
202 def default_nameid(self):
203 return self.get_config_value('default nameid')
206 def default_email_domain(self):
207 return self.get_config_value('default email domain')
209 def get_tree(self, site):
210 self.idp = self.init_idp()
211 self.page = SAML2(site, self)
212 self.admin = Saml2AdminPage(site, self)
219 idp = IdentityProvider(self)
220 except Exception, e: # pylint: disable=broad-except
221 self._debug('Failed to init SAML2 provider: %r' % e)
224 # Import all known applications
225 data = self.get_data()
228 if 'type' not in sp or sp['type'] != 'SP':
230 if 'name' not in sp or 'metadata' not in sp:
234 except Exception, e: # pylint: disable=broad-except
235 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
240 super(IdpProvider, self).on_enable()
241 self.idp = self.init_idp()
242 if hasattr(self, 'admin'):
247 class Installer(object):
249 def __init__(self, *pargs):
251 self.ptype = 'provider'
254 def install_args(self, group):
255 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
256 help='Configure SAML2 Provider')
258 def configure(self, opts):
259 if opts['saml2'] != 'yes':
262 # Check storage path is present or create it
263 path = os.path.join(opts['data_dir'], 'saml2')
264 if not os.path.exists(path):
265 os.makedirs(path, 0700)
267 # Use the same cert for signing and ecnryption for now
268 cert = Certificate(path)
269 cert.generate('idp', opts['hostname'])
271 # Generate Idp Metadata
273 if opts['secure'].lower() == 'no':
275 url = '%s://%s/%s/saml2' % (proto, opts['hostname'], opts['instance'])
276 meta = metadata.Metadata(metadata.IDP_ROLE)
277 meta.set_entity_id(url + '/metadata')
278 meta.add_certs(cert, cert)
279 meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
281 meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
282 url + '/SSO/Redirect')
284 meta.add_allowed_name_format(
285 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
286 meta.add_allowed_name_format(
287 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
288 meta.add_allowed_name_format(
289 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
290 if 'krb' in opts and opts['krb'] == 'yes':
291 meta.add_allowed_name_format(
292 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
294 meta.output(os.path.join(path, 'metadata.xml'))
296 # Add configuration data to database
297 po = PluginObject(*self.pargs)
300 po.wipe_config_values()
301 config = {'idp storage path': path,
302 'idp metadata file': 'metadata.xml',
303 'idp certificate file': cert.cert,
304 'idp key file': cert.key}
305 po.save_plugin_config(config)
307 # Update global config to add login plugin
309 po.save_enabled_state()
311 # Fixup permissions so only the ipsilon user can read these files
312 files.fix_user_dirs(path, opts['system_user'])