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.common import FACILITY
22 from ipsilon.providers.saml2.auth import AuthenticateRequest
23 from ipsilon.providers.saml2.certs import Certificate
24 from ipsilon.providers.saml2 import metadata
25 from ipsilon.util.user import UserSession
26 from ipsilon.util.plugin import PluginObject
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 session.nuke_data('login', 'Return')
62 self.stage = session.get_data('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 dump = session.get_data('saml2', 'Request')
73 self._debug("Couldn't find Request dump?!")
74 # TODO: Return to SP with auth failed error
75 raise cherrypy.HTTPError(400)
78 login = lasso.Login.newFromDump(self.cfg.idp, 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 SAML2(ProviderPageBase):
101 def __init__(self, *args, **kwargs):
102 super(SAML2, self).__init__(*args, **kwargs)
106 self.cfg.idp = lasso.Server(self.cfg.idp_metadata_file,
107 self.cfg.idp_key_file,
109 self.cfg.idp_certificate_file)
110 self.cfg.idp.role = lasso.PROVIDER_ROLE_IDP
111 except Exception, e: # pylint: disable=broad-except
112 self._debug('Failed to enable SAML2 provider: %r' % e)
115 # Import all known applications
116 data = self.cfg.get_data()
118 if 'type' not in data[idval] or data[idval]['type'] != 'SP':
120 path = os.path.join(self.cfg.idp_storage_path, str(idval))
127 meta = os.path.join(path, 'metadata.xml')
128 cert = os.path.join(path, 'certificate.pem')
129 self.cfg.idp.addProvider(lasso.PROVIDER_ROLE_SP, meta, cert)
130 self._debug('Added SP %s' % name)
131 except Exception, e: # pylint: disable=broad-except
132 self._debug('Failed to add SP %s: %r' % (name, e))
134 self.SSO = SSO(*args, **kwargs)
137 class IdpProvider(ProviderBase):
140 super(IdpProvider, self).__init__('saml2', 'saml2')
142 self.description = """
143 Provides SAML 2.0 authentication infrastructure. """
146 'idp storage path': [
147 """ Path to data storage accessible by the IdP """,
149 '/var/lib/ipsilon/saml2'
151 'idp metadata file': [
152 """ The IdP Metadata file genearated at install time. """,
156 'idp certificate file': [
157 """ The IdP PEM Certificate genearated at install time. """,
162 """ The IdP Certificate Key genearated at install time. """,
166 'allow self registration': [
167 """ Allow authenticated users to register applications. """,
171 'default allowed nameids': [
172 """Default Allowed NameIDs for Service Providers. """,
174 ['persistent', 'transient', 'email', 'kerberos', 'x509']
177 """Default NameID used by Service Providers. """,
181 'default email domain': [
182 """Default email domain, for users missing email property.""",
189 def allow_self_registration(self):
190 return self.get_config_value('allow self registration')
193 def idp_storage_path(self):
194 return self.get_config_value('idp storage path')
197 def idp_metadata_file(self):
198 return os.path.join(self.idp_storage_path,
199 self.get_config_value('idp metadata file'))
202 def idp_certificate_file(self):
203 return os.path.join(self.idp_storage_path,
204 self.get_config_value('idp certificate file'))
207 def idp_key_file(self):
208 return os.path.join(self.idp_storage_path,
209 self.get_config_value('idp key file'))
212 def default_allowed_nameids(self):
213 return self.get_config_value('default allowed nameids')
216 def default_nameid(self):
217 return self.get_config_value('default nameid')
220 def default_email_domain(self):
221 return self.get_config_value('default email domain')
223 def get_tree(self, site):
224 self.page = SAML2(site, self)
228 class Installer(object):
232 self.ptype = 'provider'
234 def install_args(self, group):
235 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
236 help='Configure SAML2 Provider')
237 group.add_argument('--saml2-storage',
238 default='/var/lib/ipsilon/saml2',
239 help='SAML2 Provider storage area')
241 def configure(self, opts):
242 if opts['saml2'] != 'yes':
245 # Check storage path is present or create it
246 path = opts['saml2_storage']
247 if not os.path.exists(path):
248 os.makedirs(path, 0700)
250 # Use the same cert for signing and ecnryption for now
251 cert = Certificate(path)
252 cert.generate('idp', opts['hostname'])
254 # Generate Idp Metadata
255 url = 'https://' + opts['hostname'] + '/idp/saml2'
256 meta = metadata.Metadata(metadata.IDP_ROLE)
257 meta.set_entity_id(url + '/metadata')
258 meta.add_certs(cert, cert)
259 meta.add_service(metadata.SSO_SERVICE,
260 lasso.SAML2_METADATA_BINDING_POST,
262 meta.add_service(metadata.SSO_SERVICE,
263 lasso.SAML2_METADATA_BINDING_REDIRECT,
266 meta.add_allowed_name_format(
267 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
268 meta.add_allowed_name_format(
269 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
270 meta.add_allowed_name_format(
271 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
272 if 'krb' in opts and opts['krb'] == 'yes':
273 meta.add_allowed_name_format(
274 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
276 meta.output(os.path.join(path, 'metadata.xml'))
278 # Add configuration data to database
283 po.wipe_config_values(FACILITY)
284 config = {'idp storage path': path,
285 'idp metadata file': 'metadata.xml',
286 'idp certificate file': cert.cert,
287 'idp key file': cert.key}
288 po.set_config(config)
289 po.save_plugin_config(FACILITY)
291 # Fixup permissions so only the ipsilon user can read these files
292 pw = pwd.getpwnam(opts['system_user'])
293 for root, dirs, files in os.walk(path):
295 target = os.path.join(root, name)
296 os.chown(target, pw.pw_uid, pw.pw_gid)
297 os.chmod(target, 0700)
299 target = os.path.join(root, name)
300 os.chown(target, pw.pw_uid, pw.pw_gid)
301 os.chmod(target, 0600)