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 SAML2(ProviderPageBase):
103 def __init__(self, *args, **kwargs):
104 super(SAML2, self).__init__(*args, **kwargs)
108 self.cfg.idp = IdentityProvider(self.cfg)
109 except Exception, e: # pylint: disable=broad-except
110 self._debug('Failed to init SAML2 provider: %r' % e)
113 # Import all known applications
114 data = self.cfg.get_data()
117 if 'type' not in sp or sp['type'] != 'SP':
119 if 'name' not in sp or 'metadata' not in sp:
122 self.cfg.idp.add_provider(sp)
123 except Exception, e: # pylint: disable=broad-except
124 self._debug('Failed to add SP %s: %r' % (sp['name'], e))
126 self.SSO = SSO(*args, **kwargs)
129 class IdpProvider(ProviderBase):
132 super(IdpProvider, self).__init__('saml2', 'saml2')
135 self.description = """
136 Provides SAML 2.0 authentication infrastructure. """
139 'idp storage path': [
140 """ Path to data storage accessible by the IdP """,
142 '/var/lib/ipsilon/saml2'
144 'idp metadata file': [
145 """ The IdP Metadata file genearated at install time. """,
149 'idp certificate file': [
150 """ The IdP PEM Certificate genearated at install time. """,
155 """ The IdP Certificate Key genearated at install time. """,
159 'allow self registration': [
160 """ Allow authenticated users to register applications. """,
164 'default allowed nameids': [
165 """Default Allowed NameIDs for Service Providers. """,
167 ['persistent', 'transient', 'email', 'kerberos', 'x509']
170 """Default NameID used by Service Providers. """,
174 'default email domain': [
175 """Default email domain, for users missing email property.""",
182 def allow_self_registration(self):
183 return self.get_config_value('allow self registration')
186 def idp_storage_path(self):
187 return self.get_config_value('idp storage path')
190 def idp_metadata_file(self):
191 return os.path.join(self.idp_storage_path,
192 self.get_config_value('idp metadata file'))
195 def idp_certificate_file(self):
196 return os.path.join(self.idp_storage_path,
197 self.get_config_value('idp certificate file'))
200 def idp_key_file(self):
201 return os.path.join(self.idp_storage_path,
202 self.get_config_value('idp key file'))
205 def default_allowed_nameids(self):
206 return self.get_config_value('default allowed nameids')
209 def default_nameid(self):
210 return self.get_config_value('default nameid')
213 def default_email_domain(self):
214 return self.get_config_value('default email domain')
216 def get_tree(self, site):
217 self.page = SAML2(site, self)
218 self.admin = AdminPage(site, self)
222 class Installer(object):
226 self.ptype = 'provider'
228 def install_args(self, group):
229 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
230 help='Configure SAML2 Provider')
231 group.add_argument('--saml2-storage',
232 default='/var/lib/ipsilon/saml2',
233 help='SAML2 Provider storage area')
235 def configure(self, opts):
236 if opts['saml2'] != 'yes':
239 # Check storage path is present or create it
240 path = opts['saml2_storage']
241 if not os.path.exists(path):
242 os.makedirs(path, 0700)
244 # Use the same cert for signing and ecnryption for now
245 cert = Certificate(path)
246 cert.generate('idp', opts['hostname'])
248 # Generate Idp Metadata
249 url = 'https://' + opts['hostname'] + '/idp/saml2'
250 meta = metadata.Metadata(metadata.IDP_ROLE)
251 meta.set_entity_id(url + '/metadata')
252 meta.add_certs(cert, cert)
253 meta.add_service(metadata.SSO_SERVICE,
254 lasso.SAML2_METADATA_BINDING_POST,
256 meta.add_service(metadata.SSO_SERVICE,
257 lasso.SAML2_METADATA_BINDING_REDIRECT,
258 url + 'SSO/Redirect')
260 meta.add_allowed_name_format(
261 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
262 meta.add_allowed_name_format(
263 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
264 meta.add_allowed_name_format(
265 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
266 if 'krb' in opts and opts['krb'] == 'yes':
267 meta.add_allowed_name_format(
268 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
270 meta.output(os.path.join(path, 'metadata.xml'))
272 # Add configuration data to database
277 po.wipe_config_values(FACILITY)
278 config = {'idp storage path': path,
279 'idp metadata file': 'metadata.xml',
280 'idp certificate file': cert.cert,
281 'idp key file': cert.key}
282 po.set_config(config)
283 po.save_plugin_config(FACILITY)
285 # Fixup permissions so only the ipsilon user can read these files
286 pw = pwd.getpwnam(opts['system_user'])
287 for root, dirs, files in os.walk(path):
289 target = os.path.join(root, name)
290 os.chown(target, pw.pw_uid, pw.pw_gid)
291 os.chmod(target, 0700)
293 target = os.path.join(root, name)
294 os.chown(target, pw.pw_uid, pw.pw_gid)
295 os.chmod(target, 0600)