diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 51d9c81..b14f912 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -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) diff --git a/src/saml2/client.py b/src/saml2/client.py index 290256c..256089e 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -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 = """%s%s""" - -def _print_statement(statem): - """ Print a statement as a HTML table """ - txt = [""""""] - 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("
") - 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() diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py index fad63f5..e645d5f 100644 --- a/src/saml2/metadata.py +++ b/src/saml2/metadata.py @@ -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"), diff --git a/src/saml2/server.py b/src/saml2/server.py index 16d6815..60abc17 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -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,