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
33 class Redirect(AuthenticateRequest):
35 def GET(self, *args, **kwargs):
37 query = cherrypy.request.query_string
39 login = self.saml2login(query)
40 return self.auth(login)
43 class POSTAuth(AuthenticateRequest):
45 def POST(self, *args, **kwargs):
47 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
48 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
50 login = self.saml2login(request)
51 login.set_msgRelayState(relaystate)
52 return self.auth(login)
55 class Continue(AuthenticateRequest):
57 def GET(self, *args, **kwargs):
59 session = UserSession()
60 user = session.get_user()
61 transdata = self.trans.retrieve()
62 self.stage = transdata['saml2_stage']
65 self._debug("User is marked anonymous?!")
66 # TODO: Return to SP with auth failed error
67 raise cherrypy.HTTPError(401)
69 self._debug('Continue auth for %s' % user.name)
71 if 'saml2_request' not in transdata:
72 self._debug("Couldn't find Request dump?!")
73 # TODO: Return to SP with auth failed error
74 raise cherrypy.HTTPError(400)
75 dump = transdata['saml2_request']
78 login = self.cfg.idp.get_login_handler(dump)
79 except Exception, e: # pylint: disable=broad-except
80 self._debug('Failed to load status from dump: %r' % e)
83 self._debug("Empty Request dump?!")
84 # TODO: Return to SP with auth failed error
85 raise cherrypy.HTTPError(400)
87 return self.auth(login)
90 class SSO(ProviderPageBase):
92 def __init__(self, *args, **kwargs):
93 super(SSO, self).__init__(*args, **kwargs)
94 self.Redirect = Redirect(*args, **kwargs)
95 self.POST = POSTAuth(*args, **kwargs)
96 self.Continue = Continue(*args, **kwargs)
99 class Metadata(ProviderPageBase):
100 def GET(self, *args, **kwargs):
101 with open(self.cfg.idp_metadata_file) as m:
103 cherrypy.response.headers["Content-Type"] = "text/xml"
104 cherrypy.response.headers["Content-Disposition"] = \
105 'attachment; filename="metadata.xml"'
109 class SAML2(ProviderPageBase):
111 def __init__(self, *args, **kwargs):
112 super(SAML2, self).__init__(*args, **kwargs)
113 self.metadata = Metadata(*args, **kwargs)
114 self.SSO = SSO(*args, **kwargs)
117 class IdpProvider(ProviderBase):
119 def __init__(self, *pargs):
120 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
124 self.description = """
125 Provides SAML 2.0 authentication infrastructure. """
131 'Path to data storage accessible by the IdP.',
132 '/var/lib/ipsilon/saml2'),
135 'The IdP Metadata file genearated at install time.',
138 'idp certificate file',
139 'The IdP PEM Certificate genearated at install time.',
143 'The IdP Certificate Key genearated at install time.',
146 'allow self registration',
147 'Allow authenticated users to register applications.',
150 'default allowed nameids',
151 'Default Allowed NameIDs for Service Providers.',
152 metadata.SAML2_NAMEID_MAP.keys(),
153 ['persistent', 'transient', 'email', 'kerberos', 'x509']),
156 'Default NameID used by Service Providers.',
157 metadata.SAML2_NAMEID_MAP.keys(),
160 'default email domain',
161 'Used for users missing the email property.',
164 if cherrypy.config.get('debug', False):
167 logger = logging.getLogger('lasso')
168 lh = logging.StreamHandler(sys.stderr)
169 logger.addHandler(lh)
170 logger.setLevel(logging.DEBUG)
173 def allow_self_registration(self):
174 return self.get_config_value('allow self registration')
177 def idp_storage_path(self):
178 return self.get_config_value('idp storage path')
181 def idp_metadata_file(self):
182 return os.path.join(self.idp_storage_path,
183 self.get_config_value('idp metadata file'))
186 def idp_certificate_file(self):
187 return os.path.join(self.idp_storage_path,
188 self.get_config_value('idp certificate file'))
191 def idp_key_file(self):
192 return os.path.join(self.idp_storage_path,
193 self.get_config_value('idp key file'))
196 def default_allowed_nameids(self):
197 return self.get_config_value('default allowed nameids')
200 def default_nameid(self):
201 return self.get_config_value('default nameid')
204 def default_email_domain(self):
205 return self.get_config_value('default email domain')
207 def get_tree(self, site):
208 self.idp = self.init_idp()
209 self.page = SAML2(site, self)
210 self.admin = Saml2AdminPage(site, self)
217 idp = IdentityProvider(self)
218 except Exception, e: # pylint: disable=broad-except
219 self._debug('Failed to init SAML2 provider: %r' % e)
222 # Import all known applications
223 data = self.get_data()
226 if 'type' not in sp or sp['type'] != 'SP':
228 if 'name' not in sp or 'metadata' not in sp:
232 except Exception, e: # pylint: disable=broad-except
233 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
238 super(IdpProvider, self).on_enable()
239 self.idp = self.init_idp()
240 if hasattr(self, 'admin'):
245 class IdpMetadataGenerator(object):
247 def __init__(self, url, idp_cert):
248 self.meta = metadata.Metadata(metadata.IDP_ROLE)
249 self.meta.set_entity_id('%s/saml2/metadata' % url)
250 self.meta.add_certs(idp_cert, idp_cert)
251 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
252 '%s/saml2/SSO/POST' % url)
253 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
254 '%s/saml2/SSO/Redirect' % url)
255 self.meta.add_allowed_name_format(
256 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
257 self.meta.add_allowed_name_format(
258 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
259 self.meta.add_allowed_name_format(
260 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
262 def output(self, path=None):
263 return self.meta.output(path)
266 class Installer(object):
268 def __init__(self, *pargs):
270 self.ptype = 'provider'
273 def install_args(self, group):
274 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
275 help='Configure SAML2 Provider')
277 def configure(self, opts):
278 if opts['saml2'] != 'yes':
281 # Check storage path is present or create it
282 path = os.path.join(opts['data_dir'], 'saml2')
283 if not os.path.exists(path):
284 os.makedirs(path, 0700)
286 # Use the same cert for signing and ecnryption for now
287 cert = Certificate(path)
288 cert.generate('idp', opts['hostname'])
290 # Generate Idp Metadata
292 if opts['secure'].lower() == 'no':
294 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
295 meta = IdpMetadataGenerator(url, cert)
296 if 'krb' in opts and opts['krb'] == 'yes':
297 meta.meta.add_allowed_name_format(
298 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
300 meta.output(os.path.join(path, 'metadata.xml'))
302 # Add configuration data to database
303 po = PluginObject(*self.pargs)
306 po.wipe_config_values()
307 config = {'idp storage path': path,
308 'idp metadata file': 'metadata.xml',
309 'idp certificate file': cert.cert,
310 'idp key file': cert.key}
311 po.save_plugin_config(config)
313 # Update global config to add login plugin
315 po.save_enabled_state()
317 # Fixup permissions so only the ipsilon user can read these files
318 files.fix_user_dirs(path, opts['system_user'])