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.provider import IdentityProvider
26 from ipsilon.providers.saml2 import metadata
27 from ipsilon.util.user import UserSession
28 from ipsilon.util.plugin import PluginObject
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 session.nuke_data('login', 'Return')
64 self.stage = session.get_data('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 dump = session.get_data('saml2', 'Request')
75 self._debug("Couldn't find Request dump?!")
76 # TODO: Return to SP with auth failed error
77 raise cherrypy.HTTPError(400)
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)
119 self.cfg.idp = IdentityProvider(self.cfg)
120 except Exception, e: # pylint: disable=broad-except
121 self._debug('Failed to init SAML2 provider: %r' % e)
124 # Import all known applications
125 data = self.cfg.get_data()
128 if 'type' not in sp or sp['type'] != 'SP':
130 if 'name' not in sp or 'metadata' not in sp:
133 self.cfg.idp.add_provider(sp)
134 except Exception, e: # pylint: disable=broad-except
135 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
137 self.SSO = SSO(*args, **kwargs)
140 class IdpProvider(ProviderBase):
143 super(IdpProvider, self).__init__('saml2', 'saml2')
146 self.description = """
147 Provides SAML 2.0 authentication infrastructure. """
150 'idp storage path': [
151 """ Path to data storage accessible by the IdP """,
153 '/var/lib/ipsilon/saml2'
155 'idp metadata file': [
156 """ The IdP Metadata file genearated at install time. """,
160 'idp certificate file': [
161 """ The IdP PEM Certificate genearated at install time. """,
166 """ The IdP Certificate Key genearated at install time. """,
170 'allow self registration': [
171 """ Allow authenticated users to register applications. """,
175 'default allowed nameids': [
176 """Default Allowed NameIDs for Service Providers. """,
178 ['persistent', 'transient', 'email', 'kerberos', 'x509']
181 """Default NameID used by Service Providers. """,
185 'default email domain': [
186 """Default email domain, for users missing email property.""",
193 def allow_self_registration(self):
194 return self.get_config_value('allow self registration')
197 def idp_storage_path(self):
198 return self.get_config_value('idp storage path')
201 def idp_metadata_file(self):
202 return os.path.join(self.idp_storage_path,
203 self.get_config_value('idp metadata file'))
206 def idp_certificate_file(self):
207 return os.path.join(self.idp_storage_path,
208 self.get_config_value('idp certificate file'))
211 def idp_key_file(self):
212 return os.path.join(self.idp_storage_path,
213 self.get_config_value('idp key file'))
216 def default_allowed_nameids(self):
217 return self.get_config_value('default allowed nameids')
220 def default_nameid(self):
221 return self.get_config_value('default nameid')
224 def default_email_domain(self):
225 return self.get_config_value('default email domain')
227 def get_tree(self, site):
228 self.page = SAML2(site, self)
229 self.admin = AdminPage(site, self)
233 class Installer(object):
237 self.ptype = 'provider'
239 def install_args(self, group):
240 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
241 help='Configure SAML2 Provider')
242 group.add_argument('--saml2-storage',
243 default='/var/lib/ipsilon/saml2',
244 help='SAML2 Provider storage area')
246 def configure(self, opts):
247 if opts['saml2'] != 'yes':
250 # Check storage path is present or create it
251 path = opts['saml2_storage']
252 if not os.path.exists(path):
253 os.makedirs(path, 0700)
255 # Use the same cert for signing and ecnryption for now
256 cert = Certificate(path)
257 cert.generate('idp', opts['hostname'])
259 # Generate Idp Metadata
260 url = 'https://' + opts['hostname'] + '/idp/saml2'
261 meta = metadata.Metadata(metadata.IDP_ROLE)
262 meta.set_entity_id(url + '/metadata')
263 meta.add_certs(cert, cert)
264 meta.add_service(metadata.SSO_SERVICE,
265 lasso.SAML2_METADATA_BINDING_POST,
267 meta.add_service(metadata.SSO_SERVICE,
268 lasso.SAML2_METADATA_BINDING_REDIRECT,
269 url + 'SSO/Redirect')
271 meta.add_allowed_name_format(
272 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
273 meta.add_allowed_name_format(
274 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
275 meta.add_allowed_name_format(
276 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
277 if 'krb' in opts and opts['krb'] == 'yes':
278 meta.add_allowed_name_format(
279 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
281 meta.output(os.path.join(path, 'metadata.xml'))
283 # Add configuration data to database
288 po.wipe_config_values(FACILITY)
289 config = {'idp storage path': path,
290 'idp metadata file': 'metadata.xml',
291 'idp certificate file': cert.cert,
292 'idp key file': cert.key}
293 po.set_config(config)
294 po.save_plugin_config(FACILITY)
296 # Fixup permissions so only the ipsilon user can read these files
297 pw = pwd.getpwnam(opts['system_user'])
298 for root, dirs, files in os.walk(path):
300 target = os.path.join(root, name)
301 os.chown(target, pw.pw_uid, pw.pw_gid)
302 os.chmod(target, 0700)
304 target = os.path.join(root, name)
305 os.chown(target, pw.pw_uid, pw.pw_gid)
306 os.chmod(target, 0600)