import datetime from hashlib import sha1 import logging from pymongo import MongoClient from pymongo.mongo_replica_set_client import MongoReplicaSetClient import pymongo.uri_parser import pymongo.errors from saml2.saml import NAMEID_FORMAT_PERSISTENT from saml2.eptid import Eptid from saml2.mdstore import InMemoryMetaData from saml2.mdstore import metadata_modules from saml2.mdstore import load_metadata_modules from saml2.s_utils import PolicyError from saml2.ident import code_binary from saml2.ident import IdentDB from saml2.ident import Unknown from saml2.mdie import to_dict from saml2.mdie import from_dict import six __author__ = 'rolandh' logger = logging.getLogger(__name__) ONTS = load_metadata_modules() MMODS = metadata_modules() class CorruptDatabase(Exception): pass def context_match(cfilter, cntx): # TODO return True class SessionStorageMDB(object): """ Session information is stored in a MongoDB database""" def __init__(self, database="", collection="assertion", **kwargs): db = _mdb_get_database(database, **kwargs) self.assertion = db[collection] def store_assertion(self, assertion, to_sign): name_id = assertion.subject.name_id nkey = sha1(code_binary(name_id)).hexdigest() doc = { "name_id_key": nkey, "assertion_id": assertion.id, "assertion": to_dict(assertion, MMODS, True), "to_sign": to_sign } _ = self.assertion.insert(doc) def get_assertion(self, cid): res = [] for item in self.assertion.find({"assertion_id": cid}): res.append({"assertion": from_dict(item["assertion"], ONTS, True), "to_sign": item["to_sign"]}) if len(res) == 1: return res[0] elif res is []: return None else: raise SystemError("More then one assertion with the same ID") def get_assertions_by_subject(self, name_id=None, session_index=None, requested_context=None): """ :param name_id: One of name_id or key can be used to get the authn statement :param session_index: If match against a session index should be done :param requested_context: Authn statements should match a specific authn context :return: """ result = [] key = sha1(code_binary(name_id)).hexdigest() for item in self.assertion.find({"name_id_key": key}): assertion = from_dict(item["assertion"], ONTS, True) if session_index or requested_context: for statement in assertion.authn_statement: if session_index: if statement.session_index == session_index: result.append(assertion) break if requested_context: if context_match(requested_context, statement.authn_context): result.append(assertion) break else: result.append(assertion) return result def remove_authn_statements(self, name_id): logger.debug("remove authn about: %s", name_id) key = sha1(code_binary(name_id)).hexdigest() for item in self.assertion.find({"name_id_key": key}): self.assertion.remove(item["_id"]) def get_authn_statements(self, name_id, session_index=None, requested_context=None): """ :param name_id: :param session_index: :param requested_context: :return: """ return [k.authn_statement for k in self.get_assertions_by_subject( name_id, session_index, requested_context)] class IdentMDB(IdentDB): def __init__(self, database="", collection="ident", domain="", name_qualifier=""): IdentDB.__init__(self, None, domain, name_qualifier) self.mdb = MDB(database=database, collection=collection) self.mdb.primary_key = "user_id" def in_store(self, _id): if [x for x in self.mdb.get(ident_id=_id)]: return True else: return False def create_id(self, nformat, name_qualifier="", sp_name_qualifier=""): _id = self._create_id(nformat, name_qualifier, sp_name_qualifier) while self.in_store(_id): _id = self._create_id(nformat, name_qualifier, sp_name_qualifier) return _id def store(self, ident, name_id): self.mdb.store(ident, name_id=to_dict(name_id, MMODS, True)) def find_nameid(self, userid, nformat=None, sp_name_qualifier=None, name_qualifier=None, sp_provided_id=None, **kwargs): # reset passed for compatibility kwargs for next usage kwargs = {} if nformat: kwargs["name_format"] = nformat if sp_name_qualifier: kwargs["sp_name_qualifier"] = sp_name_qualifier if name_qualifier: kwargs["name_qualifier"] = name_qualifier if sp_provided_id: kwargs["sp_provided_id"] = sp_provided_id res = [] for item in self.mdb.get(userid, **kwargs): res.append(from_dict(item["name_id"], ONTS, True)) return res def find_local_id(self, name_id): cnid = to_dict(name_id, MMODS, True) for item in self.mdb.get(name_id=cnid): return item[self.mdb.primary_key] return None def match_local_id(self, userid, sp_name_qualifier, name_qualifier): """ Match a local persistent identifier. Look for an existing persistent NameID matching userid, sp_name_qualifier and name_qualifier. """ filter = { "name_id.sp_name_qualifier": sp_name_qualifier, "name_id.name_qualifier": name_qualifier, "name_id.format": NAMEID_FORMAT_PERSISTENT, } res = self.mdb.get(value=userid, **filter) if not res: return None return from_dict(res[0]["name_id"], ONTS, True) def remove_remote(self, name_id): cnid = to_dict(name_id, MMODS, True) self.mdb.remove(name_id=cnid) def handle_name_id_mapping_request(self, name_id, name_id_policy): _id = self.find_local_id(name_id) if not _id: raise Unknown("Unknown entity") if name_id_policy.allow_create == "false": raise PolicyError("Not allowed to create new identifier") # else create and return a new one return self.construct_nameid(_id, name_id_policy=name_id_policy) class MDB(object): primary_key = "mdb" def __init__(self, database, collection, **kwargs): _db = _mdb_get_database(database, **kwargs) self.db = _db[collection] def store(self, value, **kwargs): if value: doc = {self.primary_key: value} else: doc = {} doc.update(kwargs) # Add timestamp to all documents to allow external garbage collecting if "created_at" not in doc: doc["created_at"] = datetime.datetime.utcnow() _ = self.db.insert(doc) def get(self, value=None, **kwargs): if value is not None: doc = {self.primary_key: value} doc.update(kwargs) return [item for item in self.db.find(doc)] elif kwargs: return [item for item in self.db.find(kwargs)] def remove(self, key=None, **kwargs): if key is None: if kwargs: for item in self.db.find(kwargs): self.db.remove(item["_id"]) else: doc = {self.primary_key: key} doc.update(kwargs) for item in self.db.find(doc): self.db.remove(item["_id"]) def keys(self): for item in self.db.find(): yield item[self.primary_key] def items(self): for item in self.db.find(): _key = item[self.primary_key] del item[self.primary_key] del item["_id"] yield _key, item def __contains__(self, key): doc = {self.primary_key: key} res = [item for item in self.db.find(doc)] if not res: return False else: return True def reset(self): self.db.drop() def _mdb_get_database(uri, **kwargs): """ Helper-function to connect to MongoDB and return a database object. The `uri' argument should be either a full MongoDB connection URI string, or just a database name in which case a connection to the default mongo instance at mongodb://localhost:27017 will be made. Performs explicit authentication if a username is provided in a connection string URI, since PyMongo does not always seem to do that as promised. :params database: name as string or (uri, name) :returns: pymongo database object """ if not "tz_aware" in kwargs: # default, but not forced kwargs["tz_aware"] = True connection_factory = MongoClient _parsed_uri = {} try: _parsed_uri = pymongo.uri_parser.parse_uri(uri) except pymongo.errors.InvalidURI: # assume URI to be just the database name db_name = uri _conn = MongoClient() pass else: if "replicaset" in _parsed_uri["options"]: connection_factory = MongoReplicaSetClient db_name = _parsed_uri.get("database", "pysaml2") _conn = connection_factory(uri, **kwargs) _db = _conn[db_name] if "username" in _parsed_uri: _db.authenticate( _parsed_uri.get("username", None), _parsed_uri.get("password", None) ) return _db #------------------------------------------------------------------------------ class EptidMDB(Eptid): def __init__(self, secret, database="", collection="eptid"): Eptid.__init__(self, secret) self.mdb = MDB(database, collection) self.mdb.primary_key = "eptid_key" def __getitem__(self, key): res = self.mdb.get(key) if not res: raise KeyError(key) elif len(res) == 1: return res[0]["eptid"] else: raise CorruptDatabase("Found more than one EPTID document") def __setitem__(self, key, value): _ = self.mdb.store(key, **{"eptid": value}) #------------------------------------------------------------------------------ def protect(dic): res = {} for key, val in dic.items(): key = key.replace(".", "__") if isinstance(val, six.string_types): pass elif isinstance(val, dict): val = protect(val) elif isinstance(val, list): li = [] for va in val: if isinstance(va, six.string_types): pass elif isinstance(va, dict): va = protect(va) # I don't think lists of lists will appear am I wrong ? li.append(va) val = li res[key] = val return res def unprotect(dic): res = {} for key, val in dic.items(): if key == "__class__": pass else: key = key.replace("__", ".") if isinstance(val, six.string_types): pass elif isinstance(val, dict): val = unprotect(val) elif isinstance(val, list): li = [] for va in val: if isinstance(va, six.string_types): pass elif isinstance(val, dict): va = unprotect(va) li.append(va) val = li res[key] = val return res def export_mdstore_to_mongo_db(mds, database, collection, sub_collection=""): mdb = MDB(database, collection, sub_collection=sub_collection) mdb.reset() mdb.primary_key = "entity_id" for key, desc in mds.items(): kwargs = { "entity_description": protect(desc), } mdb.store(key, **kwargs) class MetadataMDB(InMemoryMetaData): def __init__(self, attrc, database="", collection=""): super(MetadataMDB, self).__init__(attrc) self.mdb = MDB(database, collection) self.mdb.primary_key = "entity_id" def _ext_service(self, entity_id, typ, service, binding): try: srvs = self[entity_id][typ] except KeyError: return None if not srvs: return srvs res = [] for srv in srvs: if "extensions" in srv: for elem in srv["extensions"]["extension_elements"]: if elem["__class__"] == service: if elem["binding"] == binding: res.append(elem) return res def load(self): pass def items(self): for key, item in self.mdb.items(): yield key, unprotect(item["entity_description"]) def keys(self): return self.mdb.keys() def values(self): for key, item in self.mdb.items(): yield unprotect(item["entity_description"]) def __contains__(self, item): return item in self.mdb def __getitem__(self, item): res = self.mdb.get(item) if not res: raise KeyError(item) elif len(res) == 1: return unprotect(res[0]["entity_description"]) else: raise CorruptDatabase("More then one document with key %s" % item) def bindings(self, entity_id, typ, service): pass