From 7570a10fe93723f1fa9c9eb5c5c4ad5051b19d2c Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Wed, 5 Feb 2014 13:13:30 +0100 Subject: [PATCH] Allow for the specification of authn_instant and subject_locality in AuthnStatements. Make it possible to tell the IdP to return a response even though the SP required attributes are not present. --- src/saml2/assertion.py | 48 ++++++++++++----- src/saml2/server.py | 120 ++++++++++++++++++++++++----------------- 2 files changed, 106 insertions(+), 62 deletions(-) diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 7a78cdf..4073b6b 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -578,7 +578,8 @@ class Assertion(dict): authenticating_authority=factory( saml.AuthenticatingAuthority, text=authn_auth)) - def _authn_context_class_ref(self, authn_class, authn_auth=None): + @staticmethod + def _authn_context_class_ref(authn_class, authn_auth=None): """ Construct the authn context with a authn context class reference :param authn_class: The authn context class reference @@ -596,45 +597,62 @@ class Assertion(dict): authn_context_class_ref=cntx_class) def _authn_statement(self, authn_class=None, authn_auth=None, - authn_decl=None, authn_decl_ref=None): + authn_decl=None, authn_decl_ref=None, authn_instant="", + subject_locality=""): """ Construct the AuthnStatement :param authn_class: Authentication Context Class reference :param authn_auth: Authenticating Authority :param authn_decl: Authentication Context Declaration :param authn_decl_ref: Authentication Context Declaration reference + :param authn_instant: When the Authentication was performed. + Assumed to be seconds since the Epoch. + :param subject_locality: Specifies the DNS domain name and IP address + for the system from which the assertion subject was apparently + authenticated. :return: An AuthnContext instance """ + if authn_instant: + _instant = instant(time_stamp=authn_instant) + else: + _instant = instant() + if authn_class: - return factory( + res = factory( saml.AuthnStatement, - authn_instant=instant(), + authn_instant=_instant, session_index=sid(), authn_context=self._authn_context_class_ref( authn_class, authn_auth)) elif authn_decl: - return factory( + res = factory( saml.AuthnStatement, - authn_instant=instant(), + authn_instant=_instant, session_index=sid(), authn_context=self._authn_context_decl(authn_decl, authn_auth)) elif authn_decl_ref: - return factory( + res = factory( saml.AuthnStatement, - authn_instant=instant(), + authn_instant=_instant, session_index=sid(), authn_context=self._authn_context_decl_ref(authn_decl_ref, authn_auth)) else: - return factory( + res = factory( saml.AuthnStatement, - authn_instant=instant(), + authn_instant=_instant, session_index=sid()) + if subject_locality: + res.subject_locality = saml.SubjectLocality(text=subject_locality) + + return res + def construct(self, sp_entity_id, in_response_to, consumer_url, name_id, attrconvs, policy, issuer, authn_class=None, authn_auth=None, authn_decl=None, encrypt=None, - sec_context=None, authn_decl_ref=None): + sec_context=None, authn_decl_ref=None, authn_instant="", + subject_locality=""): """ Construct the Assertion :param sp_entity_id: The entityid of the SP @@ -651,6 +669,10 @@ class Assertion(dict): :param encrypt: Whether to encrypt parts or all of the Assertion :param sec_context: The security context used when encrypting :param authn_decl_ref: An Authentication Context declaration reference + :param authn_instant: When the Authentication was performed + :param subject_locality: Specifies the DNS domain name and IP address + for the system from which the assertion subject was apparently + authenticated. :return: An Assertion instance """ @@ -677,7 +699,9 @@ class Assertion(dict): if authn_auth or authn_class or authn_decl or authn_decl_ref: _authn_statement = self._authn_statement(authn_class, authn_auth, - authn_decl, authn_decl_ref) + authn_decl, authn_decl_ref, + authn_instant, + subject_locality) else: _authn_statement = None diff --git a/src/saml2/server.py b/src/saml2/server.py index 97dad77..212167a 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -55,9 +55,18 @@ from saml2.profile import ecp logger = logging.getLogger(__name__) +AUTHN_DICT_MAP = { + "decl": "authn_decl", + "authn_auth": "authn_auth", + "class_ref": "authn_class", + "authn_instant": "authn_instant", + "subject_locality": "subject_locality" +} + class Server(Entity): """ A class that does things that IdPs or AAs do """ + def __init__(self, config_file="", config=None, cache=None, stype="idp", symkey=""): Entity.__init__(self, stype, config, config_file) @@ -89,6 +98,7 @@ class Server(Entity): typ, data = _spec if typ.lower() == "mongodb": from saml2.mongo_store import SessionStorageMDB + return SessionStorageMDB(database=data, collection="session") raise NotImplementedError("No such storage type implemented") @@ -100,7 +110,7 @@ class Server(Entity): """ if stype == "aa": return - + # subject information is stored in a database # default database is in memory which is OK in some setups dbspec = self.config.getattr("subject_data", "idp") @@ -117,11 +127,13 @@ class Server(Entity): idb = shelve.open(addr, writeback=True) elif typ == "memcached": import memcache + idb = memcache.Client(addr) elif typ == "dict": # in-memory dictionary idb = {} elif typ == "mongodb": from saml2.mongo_store import IdentMDB + self.ident = IdentMDB(database=addr, collection="ident") if typ == "mongodb": @@ -145,6 +157,7 @@ class Server(Entity): self.eptid = EptidShelve(secret, addr) elif typ == "mongodb": from saml2.mongo_store import EptidMDB + self.eptid = EptidMDB(secret, database=addr, collection="eptid") else: @@ -257,7 +270,8 @@ class Server(Entity): def _authn_response(self, in_response_to, consumer_url, sp_entity_id, identity=None, name_id=None, status=None, authn=None, issuer=None, policy=None, - sign_assertion=False, sign_response=False): + sign_assertion=False, sign_response=False, + best_effort=False): """ Create a response. A layer of indirection. :param in_response_to: The session identifier of the request @@ -272,64 +286,62 @@ class Server(Entity): :param issuer: The issuer of the response :param sign_assertion: Whether the assertion should be signed or not :param sign_response: Whether the response should be signed or not + :param best_effort: Even if not the SPs demands can be met send a + response. :return: A response instance """ to_sign = [] args = {} - if identity: - _issuer = self._issuer(issuer) - ast = Assertion(identity) - if policy is None: - policy = Policy() - try: - ast.apply_policy(sp_entity_id, policy, self.metadata) - except MissingValue, exc: + #if identity: + _issuer = self._issuer(issuer) + ast = Assertion(identity) + if policy is None: + policy = Policy() + try: + ast.apply_policy(sp_entity_id, policy, self.metadata) + except MissingValue, exc: + if not best_effort: return self.create_error_response(in_response_to, consumer_url, exc, sign_response) - if authn: # expected to be a dictionary - if "decl" in authn: - assertion = ast.construct(sp_entity_id, in_response_to, - consumer_url, name_id, - self.config.attribute_converters, - policy, issuer=_issuer, - authn_decl=authn["decl"], - authn_auth=authn["authn_auth"]) - else: - assertion = ast.construct(sp_entity_id, in_response_to, - consumer_url, name_id, - self.config.attribute_converters, - policy, issuer=_issuer, - authn_class=authn["class_ref"], - authn_auth=authn["authn_auth"]) - else: - assertion = ast.construct(sp_entity_id, in_response_to, - consumer_url, name_id, - self.config.attribute_converters, - policy, issuer=_issuer) + if authn: # expected to be a dictionary + # Would like to use dict comprehension but ... + authn_args = dict([(AUTHN_DICT_MAP[k], + v) for k, v in authn.items()]) - if sign_assertion: - assertion.signature = pre_signature_part(assertion.id, - self.sec.my_cert, 1) - # Just the assertion or the response and the assertion ? - to_sign = [(class_name(assertion), assertion.id)] + assertion = ast.construct(sp_entity_id, in_response_to, + consumer_url, name_id, + self.config.attribute_converters, + policy, issuer=_issuer, + **authn_args) + else: + assertion = ast.construct(sp_entity_id, in_response_to, + consumer_url, name_id, + self.config.attribute_converters, + policy, issuer=_issuer) - # Store which assertion that has been sent to which SP about which - # subject. + if sign_assertion: + assertion.signature = pre_signature_part(assertion.id, + self.sec.my_cert, 1) + # Just the assertion or the response and the assertion ? + to_sign = [(class_name(assertion), assertion.id)] - # self.cache.set(assertion.subject.name_id.text, - # sp_entity_id, {"ava": identity, "authn": authn}, - # assertion.conditions.not_on_or_after) + # Store which assertion that has been sent to which SP about which + # subject. - args["assertion"] = assertion + # self.cache.set(assertion.subject.name_id.text, + # sp_entity_id, {"ava": identity, "authn": authn}, + # assertion.conditions.not_on_or_after) - if self.support_AssertionIDRequest() or self.support_AuthnQuery(): - self.session_db.store_assertion(assertion, to_sign) + args["assertion"] = assertion + + if self.support_AssertionIDRequest() or self.support_AuthnQuery(): + self.session_db.store_assertion(assertion, to_sign) return self._response(in_response_to, consumer_url, status, issuer, sign_response, to_sign, **args) - + # ------------------------------------------------------------------------ #noinspection PyUnusedLocal @@ -420,7 +432,15 @@ class Server(Entity): :return: A response instance """ - policy = self.config.getattr("policy", "idp") + try: + policy = kwargs["release_policy"] + except KeyError: + policy = self.config.getattr("policy", "idp") + + try: + best_effort = kwargs["best_effort"] + except KeyError: + best_effort = False if not name_id: try: @@ -456,15 +476,16 @@ class Server(Entity): _authn = authn return self._authn_response(in_response_to, # in_response_to - destination, # consumer_url - sp_entity_id, # sp_entity_id - identity, # identity as dictionary + destination, # consumer_url + sp_entity_id, # sp_entity_id + identity, # identity as dictionary name_id, authn=_authn, issuer=issuer, policy=policy, sign_assertion=sign_assertion, - sign_response=sign_response) + sign_response=sign_response, + best_effort=best_effort) except MissingValue, exc: return self.create_error_response(in_response_to, destination, @@ -553,7 +574,6 @@ class Server(Entity): asserts = [] for statement in self.session_db.get_authn_statements( subject.name_id, session_index, requested_context): - asserts.append(saml.Assertion(authn_statement=statement, subject=subject, **margs))