More documentation
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user