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.admin import AdminPage
24 from ipsilon.providers.saml2.certs import Certificate
25 from ipsilon.providers.saml2 import metadata
26 from ipsilon.util.user import UserSession
27 from ipsilon.util.plugin import PluginObject
34 class Redirect(AuthenticateRequest):
36 def GET(self, *args, **kwargs):
38 query = cherrypy.request.query_string
40 login = self.saml2login(query)
41 return self.auth(login)
44 class POSTAuth(AuthenticateRequest):
46 def POST(self, *args, **kwargs):
48 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
49 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
51 login = self.saml2login(request)
52 login.set_msgRelayState(relaystate)
53 return self.auth(login)
56 class Continue(AuthenticateRequest):
58 def GET(self, *args, **kwargs):
60 session = UserSession()
61 user = session.get_user()
62 session.nuke_data('login', 'Return')
63 self.stage = session.get_data('saml2', 'stage')
66 self._debug("User is marked anonymous?!")
67 # TODO: Return to SP with auth failed error
68 raise cherrypy.HTTPError(401)
70 self._debug('Continue auth for %s' % user.name)
72 dump = session.get_data('saml2', 'Request')
74 self._debug("Couldn't find Request dump?!")
75 # TODO: Return to SP with auth failed error
76 raise cherrypy.HTTPError(400)
79 login = lasso.Login.newFromDump(self.cfg.idp, dump)
80 except Exception, e: # pylint: disable=broad-except
81 self._debug('Failed to load status from dump: %r' % e)
84 self._debug("Empty Request dump?!")
85 # TODO: Return to SP with auth failed error
86 raise cherrypy.HTTPError(400)
88 return self.auth(login)
91 class SSO(ProviderPageBase):
93 def __init__(self, *args, **kwargs):
94 super(SSO, self).__init__(*args, **kwargs)
95 self.Redirect = Redirect(*args, **kwargs)
96 self.POST = POSTAuth(*args, **kwargs)
97 self.Continue = Continue(*args, **kwargs)
100 class SAML2(ProviderPageBase):
102 def __init__(self, *args, **kwargs):
103 super(SAML2, self).__init__(*args, **kwargs)
107 self.cfg.idp = lasso.Server(self.cfg.idp_metadata_file,
108 self.cfg.idp_key_file,
110 self.cfg.idp_certificate_file)
111 self.cfg.idp.role = lasso.PROVIDER_ROLE_IDP
112 except Exception, e: # pylint: disable=broad-except
113 self._debug('Failed to enable SAML2 provider: %r' % e)
116 # Import all known applications
117 data = self.cfg.get_data()
119 if 'type' not in data[idval] or data[idval]['type'] != 'SP':
121 path = os.path.join(self.cfg.idp_storage_path, str(idval))
128 meta = os.path.join(path, 'metadata.xml')
129 cert = os.path.join(path, 'certificate.pem')
130 self.cfg.idp.addProvider(lasso.PROVIDER_ROLE_SP, meta, cert)
131 self._debug('Added SP %s' % name)
132 except Exception, e: # pylint: disable=broad-except
133 self._debug('Failed to add SP %s: %r' % (name, e))
135 self.SSO = SSO(*args, **kwargs)
138 class IdpProvider(ProviderBase):
141 super(IdpProvider, self).__init__('saml2', 'saml2')
143 self.description = """
144 Provides SAML 2.0 authentication infrastructure. """
147 'idp storage path': [
148 """ Path to data storage accessible by the IdP """,
150 '/var/lib/ipsilon/saml2'
152 'idp metadata file': [
153 """ The IdP Metadata file genearated at install time. """,
157 'idp certificate file': [
158 """ The IdP PEM Certificate genearated at install time. """,
163 """ The IdP Certificate Key genearated at install time. """,
167 'allow self registration': [
168 """ Allow authenticated users to register applications. """,
172 'default allowed nameids': [
173 """Default Allowed NameIDs for Service Providers. """,
175 ['persistent', 'transient', 'email', 'kerberos', 'x509']
178 """Default NameID used by Service Providers. """,
182 'default email domain': [
183 """Default email domain, for users missing email property.""",
190 def allow_self_registration(self):
191 return self.get_config_value('allow self registration')
194 def idp_storage_path(self):
195 return self.get_config_value('idp storage path')
198 def idp_metadata_file(self):
199 return os.path.join(self.idp_storage_path,
200 self.get_config_value('idp metadata file'))
203 def idp_certificate_file(self):
204 return os.path.join(self.idp_storage_path,
205 self.get_config_value('idp certificate file'))
208 def idp_key_file(self):
209 return os.path.join(self.idp_storage_path,
210 self.get_config_value('idp key file'))
213 def default_allowed_nameids(self):
214 return self.get_config_value('default allowed nameids')
217 def default_nameid(self):
218 return self.get_config_value('default nameid')
221 def default_email_domain(self):
222 return self.get_config_value('default email domain')
224 def get_tree(self, site):
225 self.page = SAML2(site, self)
226 self.admin = AdminPage(site, self)
230 class Installer(object):
234 self.ptype = 'provider'
236 def install_args(self, group):
237 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
238 help='Configure SAML2 Provider')
239 group.add_argument('--saml2-storage',
240 default='/var/lib/ipsilon/saml2',
241 help='SAML2 Provider storage area')
243 def configure(self, opts):
244 if opts['saml2'] != 'yes':
247 # Check storage path is present or create it
248 path = opts['saml2_storage']
249 if not os.path.exists(path):
250 os.makedirs(path, 0700)
252 # Use the same cert for signing and ecnryption for now
253 cert = Certificate(path)
254 cert.generate('idp', opts['hostname'])
256 # Generate Idp Metadata
257 url = 'https://' + opts['hostname'] + '/idp/saml2'
258 meta = metadata.Metadata(metadata.IDP_ROLE)
259 meta.set_entity_id(url + '/metadata')
260 meta.add_certs(cert, cert)
261 meta.add_service(metadata.SSO_SERVICE,
262 lasso.SAML2_METADATA_BINDING_POST,
264 meta.add_service(metadata.SSO_SERVICE,
265 lasso.SAML2_METADATA_BINDING_REDIRECT,
268 meta.add_allowed_name_format(
269 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
270 meta.add_allowed_name_format(
271 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
272 meta.add_allowed_name_format(
273 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
274 if 'krb' in opts and opts['krb'] == 'yes':
275 meta.add_allowed_name_format(
276 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
278 meta.output(os.path.join(path, 'metadata.xml'))
280 # Add configuration data to database
285 po.wipe_config_values(FACILITY)
286 config = {'idp storage path': path,
287 'idp metadata file': 'metadata.xml',
288 'idp certificate file': cert.cert,
289 'idp key file': cert.key}
290 po.set_config(config)
291 po.save_plugin_config(FACILITY)
293 # Fixup permissions so only the ipsilon user can read these files
294 pw = pwd.getpwnam(opts['system_user'])
295 for root, dirs, files in os.walk(path):
297 target = os.path.join(root, name)
298 os.chown(target, pw.pw_uid, pw.pw_gid)
299 os.chmod(target, 0700)
301 target = os.path.join(root, name)
302 os.chown(target, pw.pw_uid, pw.pw_gid)
303 os.chmod(target, 0600)