summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Dennis <jdennis@redhat.com>2015-01-26 16:04:40 -0500
committerRob Crittenden <rcritten@redhat.com>2015-05-08 11:17:02 -0400
commitbe55bdf7ee36ad38b25b5f79fc4b82edb2557148 (patch)
tree27366c06367351b9c14677992f3581a01b504da1
parentcfe24fa3dc15d87f3ace944a2d62a0f4c5ee496c (diff)
downloadipsilon-be55bdf7ee36ad38b25b5f79fc4b82edb2557148.tar.gz
ipsilon-be55bdf7ee36ad38b25b5f79fc4b82edb2557148.tar.xz
ipsilon-be55bdf7ee36ad38b25b5f79fc4b82edb2557148.zip
Implement ECP in Ipsilon
* add saml2/SSO/SOAP endpoint. * add check for lasso version, ECP endpoint only exposed in metadata if lasso has full ECP support. * add SSO_SOAP soap authentication handler (used for ECP). * add SAML binding to transaction so we can determine if cookies and other HTTP concepts are expected. Each handler is responsible for setting the binding. * add some constants needed for ECP https://fedorahosted.org/ipsilon/ticket/4 Signed-off-by: John Dennis <jdennis@redhat.com> Reviewed-by: Rob Crittenden <rcritten@redhat.com>
-rw-r--r--ipsilon/providers/saml2/auth.py27
-rw-r--r--ipsilon/providers/saml2idp.py57
-rwxr-xr-xipsilon/tools/saml2metadata.py2
-rw-r--r--ipsilon/util/constants.py4
-rw-r--r--ipsilon/util/http.py68
5 files changed, 154 insertions, 4 deletions
diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py
index 5c00e97..611c9bf 100644
--- a/ipsilon/providers/saml2/auth.py
+++ b/ipsilon/providers/saml2/auth.py
@@ -6,6 +6,7 @@ from ipsilon.providers.saml2.provider import ServiceProvider
from ipsilon.providers.saml2.provider import InvalidProviderId
from ipsilon.providers.saml2.provider import NameIdNotAllowed
from ipsilon.providers.saml2.sessions import SAMLSessionsContainer
+from ipsilon.tools import saml2metadata as metadata
from ipsilon.util.policy import Policy
from ipsilon.util.user import UserSession
from ipsilon.util.trans import Transaction
@@ -29,14 +30,29 @@ class AuthenticateRequest(ProviderPageBase):
super(AuthenticateRequest, self).__init__(*args, **kwargs)
self.stage = 'init'
self.trans = None
+ self.binding = None
def _preop(self, *args, **kwargs):
try:
# generate a new id or get current one
self.trans = Transaction('saml2', **kwargs)
- if self.trans.cookie.value != self.trans.provider:
- self.debug('Invalid transaction, %s != %s' % (
- self.trans.cookie.value, self.trans.provider))
+
+ self.debug('self.binding=%s, transdata=%s' %
+ (self.binding, self.trans.retrieve()))
+ if self.binding is None:
+ # SAML binding is unknown, try to get it from transaction
+ transdata = self.trans.retrieve()
+ self.binding = transdata.get('saml2_binding')
+ else:
+ # SAML binding known, store in transaction
+ data = {'saml2_binding': self.binding}
+ self.trans.store(data)
+
+ # Only check for cookie for those bindings which use one
+ if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]):
+ if self.trans.cookie.value != self.trans.provider:
+ self.debug('Invalid transaction, %s != %s' % (
+ self.trans.cookie.value, self.trans.provider))
except Exception, e: # pylint: disable=broad-except
self.debug('Transaction initialization failed: %s' % repr(e))
raise cherrypy.HTTPError(400, 'Invalid transaction id')
@@ -303,5 +319,10 @@ class AuthenticateRequest(ProviderPageBase):
}
return self._template('saml2/post_response.html', **context)
+ elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
+ login.buildResponseMsg()
+ self.debug("Returning ECP: %s" % login.msgBody)
+ return login.msgBody
+
else:
raise cherrypy.HTTPError(500)
diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py
index ef31f36..6dfb03a 100644
--- a/ipsilon/providers/saml2idp.py
+++ b/ipsilon/providers/saml2idp.py
@@ -10,6 +10,8 @@ from ipsilon.providers.saml2.provider import IdentityProvider
from ipsilon.tools.certs import Certificate
from ipsilon.tools import saml2metadata as metadata
from ipsilon.tools import files
+from ipsilon.util.http import require_content_type
+from ipsilon.util.constants import SOAP_MEDIA_TYPE, XML_MEDIA_TYPE
from ipsilon.util.user import UserSession
from ipsilon.util.plugin import PluginObject
from ipsilon.util import config as pconfig
@@ -20,9 +22,54 @@ import os
import time
import uuid
+cherrypy.tools.require_content_type = cherrypy.Tool('before_request_body',
+ require_content_type)
+
+
+def is_lasso_ecp_enabled():
+ # Full ECP support appeared in lasso version 2.4.2
+ return lasso.checkVersion(2, 4, 2, lasso.CHECK_VERSION_NUMERIC)
+
+
+class SSO_SOAP(AuthenticateRequest):
+
+ def __init__(self, *args, **kwargs):
+ super(SSO_SOAP, self).__init__(*args, **kwargs)
+ self.binding = metadata.SAML2_SERVICE_MAP['sso-soap'][1]
+
+ @cherrypy.tools.require_content_type(
+ required=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE])
+ @cherrypy.tools.accept(media=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE])
+ @cherrypy.tools.response_headers(
+ headers=[('Content-Type', 'SOAP_MEDIA_TYPE')])
+ def POST(self, *args, **kwargs):
+ self.debug("SSO_SOAP.POST() begin")
+
+ self.debug("SSO_SOAP transaction provider=%s id=%s" %
+ (self.trans.provider, self.trans.transaction_id))
+
+ us = UserSession()
+ us.remote_login()
+ user = us.get_user()
+ self.debug("SSO_SOAP user=%s" % (user.name))
+
+ if not user:
+ raise cherrypy.HTTPError(403, 'No user specified for SSO_SOAP')
+
+ soap_xml_doc = cherrypy.request.rfile.read()
+ soap_xml_doc = soap_xml_doc.strip()
+ self.debug("SSO_SOAP soap_xml_doc=%s" % soap_xml_doc)
+ login = self.saml2login(soap_xml_doc)
+
+ return self.auth(login)
+
class Redirect(AuthenticateRequest):
+ def __init__(self, *args, **kwargs):
+ super(Redirect, self).__init__(*args, **kwargs)
+ self.binding = metadata.SAML2_SERVICE_MAP['sso-redirect'][1]
+
def GET(self, *args, **kwargs):
query = cherrypy.request.query_string
@@ -33,6 +80,10 @@ class Redirect(AuthenticateRequest):
class POSTAuth(AuthenticateRequest):
+ def __init__(self, *args, **kwargs):
+ super(POSTAuth, self).__init__(*args, **kwargs)
+ self.binding = metadata.SAML2_SERVICE_MAP['sso-post'][1]
+
def POST(self, *args, **kwargs):
request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
@@ -98,6 +149,7 @@ class SSO(ProviderPageBase):
self.Redirect = Redirect(*args, **kwargs)
self.POST = POSTAuth(*args, **kwargs)
self.Continue = Continue(*args, **kwargs)
+ self.SOAP = SSO_SOAP(*args, **kwargs)
class SLO(ProviderPageBase):
@@ -118,7 +170,7 @@ class Metadata(ProviderPageBase):
def GET(self, *args, **kwargs):
body = self._get_metadata()
- cherrypy.response.headers["Content-Type"] = "text/xml"
+ cherrypy.response.headers["Content-Type"] = XML_MEDIA_TYPE
cherrypy.response.headers["Content-Disposition"] = \
'attachment; filename="metadata.xml"'
return body
@@ -368,6 +420,9 @@ class IdpMetadataGenerator(object):
'%s/saml2/SSO/POST' % url)
self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
'%s/saml2/SSO/Redirect' % url)
+ if is_lasso_ecp_enabled():
+ self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-soap'],
+ '%s/saml2/SSO/SOAP' % url)
self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
'%s/saml2/SLO/Redirect' % url)
self.meta.add_allowed_name_format(
diff --git a/ipsilon/tools/saml2metadata.py b/ipsilon/tools/saml2metadata.py
index 3891b6f..99857bf 100755
--- a/ipsilon/tools/saml2metadata.py
+++ b/ipsilon/tools/saml2metadata.py
@@ -25,6 +25,8 @@ SAML2_SERVICE_MAP = {
lasso.SAML2_METADATA_BINDING_POST),
'sso-redirect': ('SingleSignOnService',
lasso.SAML2_METADATA_BINDING_REDIRECT),
+ 'sso-soap': ('SingleSignOnService',
+ lasso.SAML2_METADATA_BINDING_SOAP),
'logout-redirect': ('SingleLogoutService',
lasso.SAML2_METADATA_BINDING_REDIRECT),
'response-post': ('AssertionConsumerService',
diff --git a/ipsilon/util/constants.py b/ipsilon/util/constants.py
new file mode 100644
index 0000000..5de4cf8
--- /dev/null
+++ b/ipsilon/util/constants.py
@@ -0,0 +1,4 @@
+# Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
+
+SOAP_MEDIA_TYPE = 'application/soap+xml'
+XML_MEDIA_TYPE = 'text/xml'
diff --git a/ipsilon/util/http.py b/ipsilon/util/http.py
new file mode 100644
index 0000000..fa9d725
--- /dev/null
+++ b/ipsilon/util/http.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
+
+import cherrypy
+import fnmatch
+
+
+def require_content_type(required=None, absent_ok=True, debug=False):
+ '''CherryPy Tool that validates request Content-Type.
+
+ This is a CherryPy Tool that checks the Content-Type in a request and
+ raises HTTP Error 415 "Unsupported Media Type" if it does not match.
+
+ The tool accepts a glob style pattern or list of patterns (see fnmatch)
+ and verifies the Content-Type in the request matches at least one of
+ the patterns, if not a HTTP Error 415 "Unsupported Media Type" is raised.
+
+ If absent_ok is False and if the request does not contain a
+ Content-Type header a HTTP Error 415 "Unsupported Media Type" is
+ raised.
+
+ The tool may be deployed use any of the standard methods for
+ invoking CherryPy tools, for example as a decorator:
+
+ @cherrypy.tools.require_content_type(required='text/xml')
+ def POST(self, *args, **kwargs):
+ pass
+
+ :param required: May be a single string or a list of strings. Each
+ string is interpreted as a glob style pattern (see fnmatch).
+ The Content-Type must match at least one pattern.
+
+ :param absent_ok: Boolean specifying if the Content-Type header
+ must be present or if it is OK to be absent.
+
+ '''
+ if required is None:
+ return
+
+ if isinstance(required, basestring):
+ required = [required]
+
+ content_type = cherrypy.request.body.content_type.value
+ pattern = None
+ match = False
+ if content_type:
+ for pattern in required:
+ if fnmatch.fnmatch(content_type, pattern):
+ match = True
+ break
+ else:
+ if absent_ok:
+ return
+
+ if debug:
+ cherrypy.log('require_content_type: required=%s, absent_ok=%s '
+ 'content_type=%s match=%s pattern=%s' %
+ required, absent_ok, content_type, match, pattern)
+
+ if not match:
+ acceptable = ', '.join(['"%s"' % x for x in required])
+ if content_type:
+ content_type = '"%s"' % content_type
+ else:
+ content_type = 'not specified'
+ message = ('Content-Type must match one of following patterns [%s], '
+ 'but the Content-Type was %s' %
+ (acceptable, content_type))
+ raise cherrypy.HTTPError(415, message=message)