More documentation

This commit is contained in:
Roland Hedberg
2010-10-18 15:50:05 +02:00
parent 0c16b5b817
commit 5bf9926efc
4 changed files with 174 additions and 91 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Umeå University
# Copyright (C) 2010 Umeå University
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -60,7 +60,13 @@ def _filter_values(vals, vlist=None, must=False):
def filter_on_attributes(ava, required=None, optional=None):
""" Filter
:param required: list of RequestedAttribute instances
:param ava: An attribute value assertion as a dictionary
:param required: list of RequestedAttribute instances defined to be
required
:param optional: list of RequestedAttribute instances defined to be
optional
:return: The modified attribute value assertion
"""
res = {}
@@ -98,7 +104,14 @@ def filter_on_attributes(ava, required=None, optional=None):
return res
def filter_on_demands(ava, required={}, optional={}):
""" Never return more than is needed """
""" Never return more than is needed. Filters out everything
the server is prepared to return but the receiver doesn't ask for
:param ava: Attribute value assertion as a dictionary
:param required: Required attributes
:param optional: Optional attributes
:return: The possibly reduced assertion
"""
# Is all what's required there:
@@ -198,6 +211,10 @@ class Policy(object):
return self._restrictions
def get_nameid_format(self, sp_entity_id):
""" Get the NameIDFormat to used for the entity id
:param: The SP entity ID
:retur: The format
"""
try:
form = self._restrictions[sp_entity_id]["nameid_format"]
except KeyError:
@@ -209,6 +226,10 @@ class Policy(object):
return form
def get_name_form(self, sp_entity_id):
""" Get the NameFormat to used for the entity id
:param: The SP entity ID
:retur: The format
"""
form = ""
try:
@@ -222,6 +243,10 @@ class Policy(object):
return form
def get_lifetime(self, sp_entity_id):
""" The lifetime of the assertion
:param sp_entity_id: The SP entity ID
:param: lifetime as a dictionary
"""
# default is a hour
spec = {"hours":1}
if not self._restrictions:
@@ -238,6 +263,12 @@ class Policy(object):
return spec
def get_attribute_restriction(self, sp_entity_id):
""" Return the attribute restriction for SP that want the information
:param sp_entity_id: The SP entity ID
:return: The restrictions
"""
if not self._restrictions:
return None
@@ -260,6 +291,7 @@ class Policy(object):
""" When the assertion stops being valid, should not be
used after this time.
:param sp_entity_id: The SP entity ID
:return: String representation of the time
"""
@@ -305,6 +337,11 @@ class Policy(object):
return self.filter(ava, sp_entity_id, required, optional)
def conditions(self, sp_entity_id):
""" Return a saml.Condition instance
:param sp_entity_id: The SP entity ID
:return: A saml.Condition instance
"""
return factory( saml.Conditions,
not_before=instant(),
# How long might depend on who's getting it
@@ -377,4 +414,11 @@ class Assertion(dict):
)
def apply_policy(self, sp_entity_id, policy, metadata=None):
""" Apply policy to the assertion I'm representing
:param sp_entity_id: The SP entity ID
:param policy: The policy
:param metadata: Metadata to use
:return: The resulting AVA after the policy is applied
"""
return policy.restrict(self, sp_entity_id, metadata)

View File

@@ -98,7 +98,7 @@ class Saml2Client(object):
else:
self.debug = debug
def relay_state(self, session_id):
def _relay_state(self, session_id):
vals = [session_id, str(int(time.time()))]
vals.append(signature(self.config["secret"], vals))
return "|".join(vals)
@@ -110,21 +110,34 @@ class Saml2Client(object):
request.destination = destination
return request
def idp_entry(self, name=None, location=None, provider_id=None):
res = samlp.IDPEntry()
if name:
res.name = name
if location:
res.loc = location
if provider_id:
res.provider_id = provider_id
return res
def scoping_from_metadata(self, entityid, location=None):
name = self.metadata.name(entityid)
idp_ent = self.idp_entry(name, location)
return samlp.Scoping(idp_list=samlp.IDPList(idp_entry=[idp_ent]))
# def idp_entry(self, name=None, location=None, provider_id=None):
# """ Create an IDP entry
#
# :param name: The name of the IdP
# :param location: The location of the IdP
# :param provider_id: The identifier of the provider
# :return: A IdPEntry instance
# """
# res = samlp.IDPEntry()
# if name:
# res.name = name
# if location:
# res.loc = location
# if provider_id:
# res.provider_id = provider_id
#
# return res
#
# def scoping_from_metadata(self, entityid, location=None):
# """ Set the scope of the assertion
#
# :param entityid: The EntityID of the server
# :param location: The location of the server
# :return: A samlp.Scoping instance
# """
# name = self.metadata.name(entityid)
# idp_ent = self.idp_entry(name, location)
# return samlp.Scoping(idp_list=samlp.IDPList(idp_entry=[idp_ent]))
def response(self, post, entity_id, outstanding, log=None):
""" Deal with an AuthnResponse or LogoutResponse
@@ -158,7 +171,7 @@ class Saml2Client(object):
if isinstance(resp, AuthnResponse):
self.users.add_information_about_person(resp.session_info())
elif isinstance(resp, LogoutResponse):
self.handle_logout_response(resp)
self.handle_logout_response(resp, log)
return resp
@@ -529,7 +542,7 @@ class Saml2Client(object):
else:
session_id = request.id
rstate = self.relay_state(session_id)
rstate = self._relay_state(session_id)
self.state[session_id] = {"entity_id": entity_id,
"operation": "SLO",
@@ -561,17 +574,26 @@ class Saml2Client(object):
return (0, "", [], response)
def local_logout(self, subject_id):
# Remove the user from the cache, equals local logout
""" Remove the user from the cache, equals local logout
:param subject_id: The identifier of the subject
"""
self.users.remove_person(subject_id)
return True
def handle_logout_response(self, response):
""" handles a Logout response """
self.log and self.log.info("state: %s" % (self.state,))
def handle_logout_response(self, response, log):
""" handles a Logout response
:param response: A response.Response instance
:param log: A logging function
:return: 4-tuple of (session_id of the last sent logout request,
response message, response headers and message)
"""
log and log.info("state: %s" % (self.state,))
status = self.state[response.in_response_to]
self.log and self.log.info("status: %s" % (status,))
log and log.info("status: %s" % (status,))
issuer = response.issuer()
self.log and self.log.info("issuer: %s" % issuer)
log and log.info("issuer: %s" % issuer)
del self.state[response.in_response_to]
if status["entity_ids"] == [issuer]: # done
self.local_logout(status["subject_id"])
@@ -630,12 +652,17 @@ class Saml2Client(object):
if self.debug and log:
log.info(response)
return self.handle_logout_response(response)
return self.handle_logout_response(response, log)
return response
def add_vo_information_about_user(self, subject_id):
""" Add information to the knowledge I have about the user """
""" Add information to the knowledge I have about the user. This is
for Virtual organizations.
:param subject_id: The subject identifier
:return: A possibly extended knowledge.
"""
try:
(ava, _) = self.users.get_identity(subject_id)
except KeyError:
@@ -648,43 +675,7 @@ class Saml2Client(object):
ava = self.users.get_identity(subject_id)[0]
return ava
def is_session_valid(session_id):
def is_session_valid(self, session_id):
""" Place holder. Supposed to check if the session is still valid.
"""
return True
# ----------------------------------------------------------------------
ROW = """<tr><td>%s</td><td>%s</td></tr>"""
def _print_statement(statem):
""" Print a statement as a HTML table """
txt = ["""<table border="1">"""]
for key, val in statem.__dict__.items():
if key.startswith("_"):
continue
else:
if isinstance(val, basestring):
txt.append(ROW % (key, val))
elif isinstance(val, list):
for value in val:
if isinstance(val, basestring):
txt.append(ROW % (key, val))
elif isinstance(value, saml2.SamlBase):
txt.append(ROW % (key, _print_statement(value)))
elif isinstance(val, saml2.SamlBase):
txt.append(ROW % (key, _print_statement(val)))
else:
txt.append(ROW % (key, val))
txt.append("</table>")
return "\n".join(txt)
def _print_statements(states):
""" Print a list statement as HTML tables """
txt = []
for stat in states:
txt.append(_print_statement(stat))
return "\n".join(txt)
def print_response(resp):
print _print_statement(resp)
print resp.to_string()

View File

@@ -578,7 +578,7 @@ def _localized_name(val, klass):
def do_organization_info(conf):
""" decription of an organization in the configuration is
a dictionary of keys and values, where the values might be tuples.
"organization": {
"name": ("AB Exempel", "se"),
"display_name": ("AB Exempel", "se"),

View File

@@ -25,9 +25,12 @@ import sys
from saml2 import saml
from saml2 import class_name
from saml2 import soap
from saml2 import request
from saml2 import BINDING_HTTP_REDIRECT, BINDING_SOAP
from saml2.request import AuthnRequest
from saml2.request import AttributeQuery
from saml2.request import LogoutRequest
from saml2.s_utils import sid
from saml2.s_utils import response_factory, logoutresponse_factory
from saml2.s_utils import MissingValue
@@ -89,7 +92,7 @@ class Identifier(object):
while True:
temp_id = sid()
try:
l = self._get_local("persistent", entity_id, temp_id)
self._get_local("persistent", entity_id, temp_id)
except KeyError:
break
self._store("persistent", entity_id, subject_id, temp_id)
@@ -133,11 +136,17 @@ class Identifier(object):
text=subj_id)
def transient_nameid(self, sp_name_qualifier, userid):
""" Returns a random one-time identifier """
""" Returns a random one-time identifier. One-time means it is
kept around as long as the session is active.
:param sp_name_qualifier: A qualifier to bind the created identifier to
:param userid: The local persistent identifier for the subject.
:return: The created identifier,
"""
while True:
temp_id = sid()
try:
l = self._get_local("transient", sp_name_qualifier, temp_id)
_ = self._get_local("transient", sp_name_qualifier, temp_id)
except KeyError:
break
self._store("transient", sp_name_qualifier, userid, temp_id)
@@ -170,7 +179,7 @@ class Identifier(object):
return self.transient_nameid(sp_entity_id, userid)
def local_name(self, entity_id, remote_id):
""" Only works for persistent names
""" Get the local persistent name that has the specified remote ID.
:param entity_id: The identifier of the entity that got the remote id
:param remote_id: The identifier that was exported
@@ -208,7 +217,10 @@ class Server(object):
# self.cache = Cache()
def load_config(self, config_file):
""" Load the server configuration
:param config_file: The name of the configuration file
"""
self.conf = IDPConfig()
self.conf.load_file(config_file)
if "subject_data" in self.conf:
@@ -227,6 +239,8 @@ class Server(object):
"""Parse a Authentication Request
:param enc_request: The request in its transport format
:param binding: Which binding that was used to transport the message
to this entity.
:return: A dictionary with keys:
consumer_url - as gotten from the SPs entity_id and the metadata
id - the id of the request
@@ -239,7 +253,7 @@ class Server(object):
receiver_addresses = self.conf.endpoint("idp",
"single_sign_on_service",
binding)
authn_request = request.AuthnRequest(self.sec,
authn_request = AuthnRequest(self.sec,
self.conf.attribute_converters(),
receiver_addresses)
authn_request = authn_request.loads(enc_request)
@@ -287,7 +301,7 @@ class Server(object):
return response
def wants(self, sp_entity_id):
""" Returns what attributes this SP requiers and which are optional
""" Returns what attributes the SP requiers and which are optional
if any such demands are registered in the Metadata.
:param sp_entity_id: The entity id of the SP
@@ -305,7 +319,7 @@ class Server(object):
query - the whole query
"""
receiver_addresses = self.conf.endpoint("aa", "attribute_service")
attribute_query = request.AttributeQuery( self.sec, receiver_addresses)
attribute_query = AttributeQuery( self.sec, receiver_addresses)
attribute_query = attribute_query.loads(xml_string)
attribute_query = attribute_query.verify()
@@ -323,8 +337,8 @@ class Server(object):
policy=Policy(), authn=None):
""" Create a Response that adhers to the ??? profile.
:param consumer_url: The URL which should receive the response
:param in_response_to: The session identifier of the request
:param consumer_url: The URL which should receive the response
:param sp_entity_id: The entity identifier of the SP
:param identity: A dictionary with attributes and values that are
expected to be the bases for the assertion in the response.
@@ -396,7 +410,19 @@ class Server(object):
def do_response(self, in_response_to, consumer_url,
sp_entity_id, identity=None, name_id=None,
status=None, sign=False, authn=None ):
""" Create a response. A layer of indirection.
:param in_response_to: The session identifier of the request
:param consumer_url: The URL which should receive the response
:param sp_entity_id: The entity identifier of the SP
:param identity: A dictionary with attributes and values that are
expected to be the bases for the assertion in the response.
:param name_id: The identifier of the subject
:param status: The status of the response
:param sign: Whether the assertion should be signed or not
:param auth: A 2-tuple denoting the authn class and the authn authority.
:return: A Response instance.
"""
try:
policy = self.conf.idp_policy()
except KeyError:
@@ -410,14 +436,14 @@ class Server(object):
def error_response(self, in_response_to, destination, spid, info,
name_id=None):
"""
:param destination: The intended recipient of this message
""" Create a error response.
:param in_response_to: The identifier of the message this is a response
to.
:param destination: The intended recipient of this message
:param spid: The entitiy ID of the SP that will get this.
:param info: Either an Exception instance or a 2-tuple consisting of
error code and descriptive text
error code and descriptive text
:return: A Response instance
"""
status = error_status_factory(info)
@@ -436,7 +462,20 @@ class Server(object):
def do_aa_response(self, in_response_to, consumer_url, sp_entity_id,
identity=None, userid="", name_id=None, status=None,
sign=False, _name_id_policy=None):
""" Create an attribute assertion response.
:param in_response_to: The session identifier of the request
:param consumer_url: The URL which should receive the response
:param sp_entity_id: The entity identifier of the SP
:param identity: A dictionary with attributes and values that are
expected to be the bases for the assertion in the response.
:param userid: A identifier of the user
:param name_id: The identifier of the subject
:param status: The status of the response
:param sign: Whether the assertion should be signed or not
:param name_id_policy: Policy for NameID creation.
:return: A Response instance.
"""
name_id = self.ident.construct_nameid(self.conf.aa_policy(), userid,
sp_entity_id, identity)
@@ -515,7 +554,7 @@ class Server(object):
slo = self.conf.endpoint("idp", "single_logout_service", binding)[0]
self.log and self.log.info("Endpoint: %s" % (slo))
req = request.LogoutRequest(self.sec, slo)
req = LogoutRequest(self.sec, slo)
if binding == BINDING_SOAP:
lreq = soap.parse_soap_enveloped_saml_logout_request(text)
try:
@@ -529,11 +568,7 @@ class Server(object):
self.log.error("%s" % (exc,))
return None
self.log and self.log.info("Before verify %s" % (req,))
req = req.verify()
self.log and self.log.info("After verify %s" % (req,))
if not req: # Not a valid request
# return a error message with status code element set to
@@ -545,9 +580,19 @@ class Server(object):
def logout_response(self, request, bindings, status=None,
sign=False):
""" Create a LogoutResponse. What is returned depends on which binding
is used.
:param request: The request the is a response to
:param bindings: Which bindings that can be used to send the response
:param status: The return status of the response operation
:return: A 3-tuple consisting of HTTP return code, HTTP headers and
possibly a message.
"""
sp_entity_id = request.issuer.text.strip()
binding = None
destination = ""
for binding in bindings:
destination = self.conf.logout_service(sp_entity_id, "sp",
binding)
@@ -564,8 +609,9 @@ class Server(object):
# Pick the first
destination = destination[0]
self.log and self.log.info("Destination: %s, binding: %s" % (destination,
binding))
if self.log:
self.log.info("Destination: %s, binding: %s" % (destination,
binding))
if not status:
status = success_status_factory()
@@ -576,6 +622,7 @@ class Server(object):
if binding == BINDING_SOAP:
response = logoutresponse_factory(
sign=sign,
id = mid,
in_response_to = request.id,
status = status,
@@ -583,6 +630,7 @@ class Server(object):
(headers, message) = http_soap_message(response)
else:
response = logoutresponse_factory(
sign=sign,
id = mid,
in_response_to = request.id,
status = status,