Files
deb-python-pysaml2/src/s2repoze/plugins/sp.py
Roland Hedberg 8e23bd2b0a Refactoring spree
2010-03-25 17:07:01 +01:00

393 lines
14 KiB
Python

# Copyright (C) 2009 Umea University
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
A plugin that allows you to use SAML2 SSO as authentication
and SAML2 attribute aggregations as metadata collector in your
WSGI application.
"""
import cgi
import os
from paste.httpexceptions import HTTPTemporaryRedirect
from paste.httpexceptions import HTTPNotImplemented
from paste.httpexceptions import HTTPInternalServerError
from paste.request import parse_dict_querystring
from paste.request import construct_url
from zope.interface import implements
from repoze.who.interfaces import IChallenger, IIdentifier, IAuthenticator
from repoze.who.interfaces import IMetadataProvider
from repoze.who.plugins.form import FormPluginBase
from saml2.client import Saml2Client
from saml2.attribute_resolver import AttributeResolver
from saml2.config import Config
from saml2.cache import Cache
def construct_came_from(environ):
""" The URL that the user used when the process where interupted
for single-sign-on processing. """
came_from = environ.get("PATH_INFO")
qstr = environ.get("QUERY_STRING","")
if qstr:
came_from += '?' + qstr
return came_from
# FormPluginBase defines the methods remember and forget
class SAML2Plugin(FormPluginBase):
implements(IChallenger, IIdentifier, IAuthenticator, IMetadataProvider)
def __init__(self, rememberer_name, saml_conf_file, virtual_organization,
wayf, cache, debug):
FormPluginBase.__init__(self)
self.rememberer_name = rememberer_name
self.debug = debug
self.wayf = wayf
self.conf = Config()
self.conf.load_file(saml_conf_file)
self.srv = self.conf["service"]["sp"]
self.log = None
if virtual_organization:
self.vorg = virtual_organization
self.vorg_conf = None
# try:
# self.vorg_conf = self.conf[
# "virtual_organization"][virtual_organization]
# except KeyError:
# self.vorg = None
else:
self.vorg = None
try:
self.metadata = self.conf["metadata"]
except KeyError:
self.metadata = None
self.outstanding_authn = {}
self.iam = os.uname()[1]
if cache:
self.cache = Cache(cache)
else:
self.cache = Cache()
def _cache_session(self, session_info):
name_id = session_info["ava"]["__userid"]
del session_info["ava"]["__userid"]
issuer = session_info["issuer"]
del session_info["issuer"]
self.cache.set(name_id, issuer, session_info,
session_info["not_on_or_after"])
return name_id
def _pick_idp(self, environ):
"""
If more than one idp and if none is selected, I have to do wayf
"""
self.log and self.log.info("IdP URL: %s" % self.srv["idp"].values())
if len( self.srv["idp"] ) == 1:
# Keys are entity_ids and values are urls
idp_url = self.srv["idp"].values()[0]
elif len( self.srv["idp"] ) == 0:
HTTPInternalServerError(detail='Misconfiguration')
else:
if self.wayf:
wayf_selected = environ.get('s2repose.wayf_selected','')
if not wayf_selected:
#self.log.info("env, keys: %s" % (environ.keys()))
return HTTPTemporaryRedirect(headers = [('Location',
self.wayf)])
else:
self.log.info("Choosen IdP: '%s'" % wayf_selected)
idp_url = self.srv["idp"][wayf_selected]
else:
HTTPNotImplemented(detail='No WAYF present!')
return idp_url
#### IChallenger ####
def challenge(self, environ, _status, _app_headers, _forget_headers):
# this challenge consist in loggin out
if environ.has_key('rwpc.logout'):
# TODO
pass
self.log = environ.get('repoze.who.logger','')
# Which page was accessed to get here
came_from = construct_came_from(environ)
if self.debug:
self.log and self.log.info("RelayState >> %s" % came_from)
# Am I part of a virtual organization ?
try:
vorg = environ["myapp.vo"]
except KeyError:
vorg = self.vorg
self.log and self.log.info("VO: %s" % vorg)
# If more than one idp and if none is selected, I have to do wayf
idp_url = self._pick_idp(environ)
# Do the AuthnRequest
scl = Saml2Client(environ, self.conf)
(sid, result) = scl.authenticate(self.conf["entityid"],
idp_url,
self.srv["url"],
self.srv["name"],
relay_state=came_from,
log=self.log,
vorg=vorg)
# remember the request
self.outstanding_authn[sid] = came_from
if self.debug:
self.log and self.log.info('sc returned: %s' % (result,))
if isinstance(result, tuple):
return HTTPTemporaryRedirect(headers=[result])
else :
HTTPInternalServerError(detail='Incorrect returned data')
def _get_post(self, environ):
""" Get the posted information """
post_env = environ.copy()
post_env['QUERY_STRING'] = ''
try:
if environ["CONTENT_LENGTH"]:
len = int(environ["CONTENT_LENGTH"])
body = environ["wsgi.input"].read(len)
from StringIO import StringIO
environ['wsgi.input'] = StringIO(body)
environ['s2repoze.body'] = body
except KeyError:
pass
post = cgi.FieldStorage(
fp=environ['wsgi.input'],
environ=post_env,
keep_blank_values=True
)
if self.debug:
self.log and self.log.info('identify post: %s' % (post,))
return post
def _construct_identity(self, name_id, session_info):
identity = {}
identity["login"] = name_id
identity["password"] = ""
identity['repoze.who.userid'] = name_id
identity["user"] = session_info["ava"]
if self.debug:
self.log and self.log.info("Identity: %s" % identity)
return identity
#### IIdentifier ####
def identify(self, environ):
self.log = environ.get('repoze.who.logger','')
uri = environ.get('REQUEST_URI', construct_url(environ))
if self.debug:
#self.log and self.log.info("environ.keys(): %s" % environ.keys())
#self.log and self.log.info("Environment: %s" % environ)
self.log and self.log.info('identify uri: %s' % (uri,))
query = parse_dict_querystring(environ)
if self.debug:
self.log and self.log.info('identify query: %s' % (query,))
post = self._get_post(environ)
# Not for me, put the post back where next in line can find it
try:
if not post.has_key("SAMLResponse"):
environ["post.fieldstorage"] = post
return {}
except TypeError:
environ["post.fieldstorage"] = post
return {}
# check for SAML2 authN response
scl = Saml2Client(environ, self.conf)
try:
# Evaluate the response
session_info = scl.response(post, self.conf["entityid"],
self.outstanding_authn,
self.log)
# Cache it
name_id = self._cache_session(session_info)
if self.debug:
self.log and self.log.info("stored %s with key %s" % (
session_info, name_id))
except TypeError:
return None
if session_info["came_from"]:
if self.debug:
self.log and self.log.info(
"came_from << %s" % session_info["came_from"])
try:
path, query = session_info["came_from"].split('?')
environ["PATH_INFO"] = path
environ["QUERY_STRING"] = query
except ValueError:
environ["PATH_INFO"] = session_info["came_from"]
environ["s2repoze.sessioninfo"] = session_info
# contruct and return the identity
return self._construct_identity(name_id, session_info)
def _vo_members_to_ask(self, subject_id):
# Find the member of the Virtual Organization that I haven't
# alrady spoken too
vo_members = [
member for member in self.metadata.vo_members(self.vorg)\
if member not in self.srv["idp"].keys()]
self.log and self.log.info("VO members: %s" % vo_members)
# Remove the ones I have cached data from about this subject
vo_members = [m for m in vo_members \
if not self.cache.active(subject_id, m)]
self.log and self.log.info(
"VO members (not cached): %s" % vo_members)
return vo_members
def _do_vo_aggregation(self, subject_id):
if self.log:
self.log.info("** Do VO aggregation **")
self.log.info("SubjectID: %s, VO:%s" % (subject_id, self.vorg))
vo_members = self._vo_members_to_ask(subject_id)
if vo_members:
# Find the NameIDFormat and the SPNameQualifier
if self.vorg_conf and "name_id_format" in self.vorg_conf:
name_id_format = self.vorg_conf["name_id_format"]
sp_name_qualifier = ""
else:
sp_name_qualifier = self.vorg
name_id_format = ""
resolver = AttributeResolver(environ, self.metadata, self.conf)
# extends returns a list of session_infos
for session_info in resolver.extend(subject_id,
self.conf["entityid"], vo_members,
name_id_format=name_id_format,
sp_name_qualifier=sp_name_qualifier,
log=self.log):
_ignore = self._cache_session(session_info)
self.log.info(
">Issuers: %s" % self.cache.entities(subject_id))
self.log.info(
"AVA: %s" % (self.cache.get_identity(subject_id),))
return True
else:
return False
# IMetadataProvider
def add_metadata(self, environ, identity):
""" Add information to the knowledge I have about the user """
subject_id = identity['repoze.who.userid']
self.log = environ.get('repoze.who.logger','')
if self.debug and self.log:
self.log.info(
"add_metadata for %s" % subject_id)
self.log.info(
"Known subjects: %s" % self.cache.subjects())
try:
self.log.info(
"Issuers: %s" % self.cache.entities(subject_id))
except KeyError:
pass
if "user" not in identity:
identity["user"] = {}
try:
(ava, _) = self.cache.get_identity(subject_id)
#now = time.gmtime()
if self.debug:
self.log and self.log.info("Adding %s" % ava)
identity["user"].update(ava)
except KeyError:
pass
if "pysaml2_vo_expanded" not in identity:
# is this a Virtual Organization situation
if self.vorg:
if self._do_vo_aggregation(subject_id):
# Get the extended identity
identity["user"] = self.cache.get_identity(subject_id)[0]
# Only do this once, mark that the identity has been
# expanded
identity["pysaml2_vo_expanded"] = 1
# @return
# used 2 times : one to get the ticket, the other to validate it
def _serviceURL(self, environ, qstr=None):
if qstr != None:
url = construct_url(environ, querystring = qstr)
else:
url = construct_url(environ)
return url
#### IAuthenticatorPlugin ####
def authenticate(self, environ, identity=None):
if identity:
return identity.get('login', None)
else:
return None
def make_plugin(rememberer_name=None, # plugin for remember
cache= "", # cache
# Which virtual organization to support
virtual_organization="",
saml_conf="",
wayf="",
debug=0,
):
if saml_conf is None:
raise ValueError(
'must include saml_conf in configuration')
if rememberer_name is None:
raise ValueError(
'must include rememberer_name in configuration')
plugin = SAML2Plugin(rememberer_name, saml_conf,
virtual_organization, wayf, cache, debug)
return plugin
# came_from = re.sub(r'ticket=[^&]*&?', '', came_from)