123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- #-*- Coding: utf-8 -*-
-
- import copy
- import sys
- import warnings
-
- from lodel.settings import Settings
- from lodel import logger
- from lodel.plugin.hooks import LodelHook
- from lodel.plugin import SessionHandlerPlugin as SessionHandler
- from .exceptions import *
-
- ##@brief Class designed to handle sessions and its datas
- class LodelSession(object):
-
- ##@brief Try to restore or to create a session
- #@param token None | mixed : If None no session will be loaded nor created
- #restore an existing session
- #@throw ClientAuthenticationFailure if a session restore fails
- def __init__(self, token = None):
- ##@brief Stores the session token
- self.__token = token
- ##@brief Stores the session datas
- self.__datas = dict()
- if token is not None:
- self.restore(token)
-
- ##@brief A token reference checker to ensure token deletion at the end of
- #a session
- #@warning Cause maybe a performance issue !
- def __token_checker(self):
- refcount = sys.getrefcount(self.__token)
- if self.__token is not None and refcount > 1:
- warnings.warn("More than one reference to the session token \
- exists ! (exactly %d)" % refcount)
-
- ##@brief Property. Equals True if a session is started else False
- @property
- def started(self):
- res = self.__token is not None
- if res:
- self.__token_checker()
- return res
-
- ##@brief Property that ensure ro acces to sessions datas
- @property
- def datas(self):
- return copy.copy(self.__datas)
-
- ##@brief Return the session token
- def retrieve_token(self):
- # DO NOT COPY THE TOKEN TO ENSURE THAT NOT MORE THAN ONE REFERENCE TO
- # IT EXISTS
- return self.__token
-
- ##@brief Late restore of a session
- #@param token mixed : the session token
- #@throw ClientAuthenticationError if a session was allready started
- #@throw ClientAuthenticationFailure if no session exists with this token
- def restore(self, token):
- if self.started:
- raise ClientAuthenticationError("Trying to restore a session, but \
- a session is allready started !!!")
- self.__datas = SessionHandler.restore(token)
- self.__token = token
- return self.datas
-
- ##@brief Save the current session state
- def save(self):
- if not self.started:
- raise ClientAuthenticationError(
- "Trying to save a non started session")
- SessionHandler.save(self.__token, self.__datas)
-
- ##@brief Destroy a session
- def destroy(self):
- if not self.started:
- logger.debug("Destroying a session that is not started")
- else:
- SessionHandler.destroy(self.__token)
- self.__token = None
- self.__datas = dict()
-
- ##@brief Destructor
- def __del__(self):
- del(self.__token)
- del(self.__datas)
-
- ##@brief Implements setter for dict access to instance
- #@todo Custom exception throwing
- def __setitem__(self, key, value):
- self.__init_session() #Start the sesssion
- self.__datas[key] = value
-
- ##@brief Implements destructor for dict access to instance
- #@todo Custom exception throwing
- def __delitem__(self, key):
- if not self.started:
- raise ClientAuthenticationError(
- "Data read access to a non started session is not possible")
- del(self.__datas[key])
-
- ##@brief Implements getter for dict acces to instance
- #@todo Custom exception throwing
- def __getitem__(self, key):
- if not self.started:
- raise ClientAuthenticationError(
- "Data read access to a non started session is not possible")
- return self.__datas[key]
-
- ##@brief Start a new session
- #@note start a new session only if no session started yet
- def __init_session(self):
- if self.__token is not None:
- return
- self.__token = SessionHandler.start()
-
-
-
- ##@brief Client metaclass designed to implements container accessor on
- #Client Class
- #
- #@todo Maybe we can delete this metaclass....
- class ClientMetaclass(type):
-
- def __init__(self, name, bases, attrs):
- return super(ClientMetaclass, self).__init__(name, bases, attrs)
-
- def __getitem__(self, key):
- return self.session()[key]
-
- def __delitem__(self, key):
- del(self.session()[key])
-
- def __setitem__(self, key, value):
- self.session()[key] = value
-
- def __str__(self):
- return str(self._instance)
-
- ##@brief Abstract singleton class designed to handle client informations
- #
- # This class is designed to handle client authentication and sessions
- class Client(object, metaclass = ClientMetaclass):
-
- ##@brief Singleton instance
- _instance = None
- ##@brief List of dict that stores field ref for login and password
- #
- # Storage specs :
- #
- # A list of dict, with keys 'login' and 'password', items are tuple.
- #- login tuple contains (LeObjectChild, FieldName, link_field) with:
- # - LeObjectChild the dynclass containing the login
- # - Fieldname the fieldname of LeObjectChild containing the login
- # - link_field None if both login and password are in the same
- # LeObjectChild. Else contains the field that make the link between
- # login LeObject and password LeObject
- #- password typle contains (LeObjectChild, FieldName)
- _infos_fields = None
-
- ##@brief Constant that stores the session key that stores authentication
- #informations
- _AUTH_DATANAME = '__auth_user_infos'
-
-
- ##@brief Constructor
- #@param session_token mixed : Session token provided by client to interface
- def __init__(self,session_token = None):
- if self.__class__ == Client:
- raise NotImplementedError("Abstract class")
- logger.debug("New instance of Client child class %s" %
- self.__class__.__name__)
- if Client._instance is not None:
- old = Client._instance
- Client._instance = None
- del(old)
- logger.debug("Replacing old Client instance by a new one")
- else:
- #first instanciation, fetching settings
- self.fetch_settings()
- ##@brief Stores infos for authenticated users (None == anonymous)
- self.__user = None
- ##@brief Stores the session handler
- Client._instance = self
- ##@brief Stores LodelSession instance
- self.__session = LodelSession(session_token)
- logger.debug("New client : %s" % self)
-
- def __del__(self):
- del(self.__session)
-
- ##@brief Try to authenticate a user with a login and a password
- #@param login str : provided login
- #@param password str : provided password (hash)
- #@warning brokes composed UID
- #@note implemets multiple login/password sources (useless ?)
- #@todo composed UID broken method
- #@todo allow to provide an authentication source
- @classmethod
- def authenticate(self, login = None, password = None):
- #Authenticate
- for infos in self._infos_fields:
- login_cls = infos['login'][0]
- pass_cls = infos['pass'][0]
- qfilter = "{passfname} = {passhash}"
- uid_fname = login_cls.uid_fieldname()[0] #COMPOSED UID BROKEN
- if login_cls == pass_cls:
- #Same EmClass for login & pass
- qfilter = qfilter.format(
- passfname = infos['pass'][1],
- passhash = password)
- else:
- #Different EmClass, building a relational filter
- passfname = "%s.%s" % (infos['login'][2], infos['pass'][1])
- qfilter = qfilter.format(
- passfname = passfname,
- passhash = password)
- getq = LeGetQuery(infos['login'][0], qfilter,
- field_list = [uid_fname], limit = 1)
- req = getq.execute()
- if len(req) == 1:
- #Authenticated
- self.__set_authenticated(infos['login'][0], req[uid_fname])
- break
- if self.is_anon():
- self.fail() #Security logging
-
- ##@brief Attempt to restore a session given a session token
- #@param token mixed : a session token
- #@return Session datas (a dict)
- #@throw ClientAuthenticationFailure if token is not valid or not
- #existing
- @classmethod
- def restore_session(cls, token):
- cls._assert_instance()
- return Client._instance.__session.restore(token)
-
- ##@brief Return the current session token or None
- #@return A session token or None
- @classmethod
- def session_token(cls):
- cls._assert_instance()
- return Client._instance.__session.retrieve_token()
-
- @classmethod
- def session(cls):
- cls._assert_instance()
- return Client._instance.__session
-
- ##@brief Delete current session
- @classmethod
- def destroy(cls):
- cls._assert_instance()
- Client._instance.__session.destroy()
-
- ##@brief Delete current client and save its session
- @classmethod
- def clean(cls):
- if Client._instance.__session.started:
- Client._instance.__session.save()
- if Client._instance is not None:
- del(Client._instance)
- Client._instance = None
-
- ##@brief Test wether a client is anonymous or logged in
- #@return True if client is anonymous
- @classmethod
- def is_anonymous(cls):
- cls._assert_instance()
- return Client._instance
-
- ##@brief Method to call on authentication failure
- #@throw ClientAuthenticationFailure
- #@throw LodelFatalError if no Client child instance found
- @classmethod
- def authentication_failure(cls):
- cls._generic_error(ClientAuthenticationFailure)
-
- ##@brief Method to call on authentication error
- #@throw ClientAuthenticationError
- #@throw LodelFatalError if no Client child instance found
- @classmethod
- def authentication_error(cls, msg = "Unknow error"):
- cls._generic_error(ClientAuthenticationError, msg)
-
- ##@brief Method to call on permission denied error
- #@throw ClientPermissionDenied
- #@throw LodelFatalError if no Client child instance found
- @classmethod
- def permission_denied_error(cls, msg = ""):
- cls._generic_error(ClientPermissionDenied, msg)
-
- ##@brief Generic error method
- #@see Client::authentication_failure() Client::authentication_error()
- #Client::permission_denied_error()
- #@throw LodelFatalError if no Client child instance found
- @classmethod
- def _generic_error(cls, expt, msg = ""):
- cls._assert_instance()
- raise expt(Client._instance, msg)
-
- ##@brief Assert that an instance of Client child class exists
- #@throw LodelFataError if no instance of Client child class found
- @classmethod
- def _assert_instance(cls):
- if Client._instance is None:
- raise LodelFatalError("No client instance found. Abording.")
-
- ##@brief Class method that fetches conf
- #
- #This method populates Client._infos_fields . This attribute stores
- #informations on login and password location (LeApi object & field)
- @classmethod
- def fetch_settings(cls):
- from lodel import dyncode
- if cls._infos_fields is None:
- cls._infos_fields = list()
- else:
- #Allready fetched
- return
- infos = (
- Settings.auth.login_classfield,
- Settings.auth.pass_classfield)
- res_infos = []
- for clsname, fieldname in infos:
- dcls = dyncode.lowername2class(infos[0][0])
- res_infos.append((dcls, infos[1][1]))
-
- link_field = None
- if res_infos[0][0] != res_infos[1][0]:
- # login and password are in two separated EmClass
- # determining the field that links login EmClass to password
- # EmClass
- for fname, fdh in res_infos[0][0].fields(True).items():
- if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
- link_field = fname
- if link_field is None:
- #Unable to find link between login & password EmClasses
- raise AuthenticationError("Unable to find a link between \
- login EmClass '%s' and password EmClass '%s'. Abording..." % (
- res_infos[0][0], res_infos[1][0]))
- res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
- cls._infos_fields.append(
- {'login':res_infos[0], 'password':res_infos[1]})
-
- ##@brief Set a user as authenticated and start a new session
- #@param leo LeObject child class : the LeObject the user is stored in
- #@param uid str : uniq id (in leo)
- #@return None
- def __set_authenticated(self, leo, uid):
- self.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}
- #Store auth infos in session
- self.__session[self.__class__._AUTH_DATANAME] = copy.copy(self.__user)
-
-
|