123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- #-*- coding: utf-8 -*-
-
- from lodel.settings import Settings
- from lodel import logger
- from lodel.plugin.hooks import LodelHook
- from lodel.leapi.query import LeGetQuery
- from lodel.exceptions import *
- from .exceptions import *
-
- ##@brief Abstract class designed to be implemented by plugin interfaces
- #
- #A singleton class storing current client informations.
- #
- #For the moment the main goal is to be able to produce security log
- #containing well formated client informations
- class Client(object):
-
- ##@brief Stores the singleton instance
- _instance = None
-
- ##@brief Implements singleton behavior
- def __init__(self):
- if self.__class__ == Client:
- raise NotImplementedError("Abstract class")
- logger.debug("New instance of %s" % self.__class__.__name__)
- if self._instance is not None:
- old = self._instance
- self._instance = None
- del(old)
- logger.debug("Replacing old Client instance by a new one")
- Client._instance = self
- logger.debug("New client : %s" % self)
-
- # Instanciation done. Triggering Auth instanciation
- self.__auth = Auth(self)
-
- ##@brief Destructor
- #@note calls Auth destructor too
- def __del__(self):
- del(self.__auth)
-
- ##@brief Abstract method.
- #
- #@note Used to generate security log message. Avoid \n etc.
- def __str__(self):
- raise NotImplementedError("Abstract method")
-
- #
- # Utility methods (Wrapper for Auth)
- #
-
- ##@brief Return current instance or raise an Exception
- #@throw AuthenticationError
- @classmethod
- def client(cls):
- if cls._instance is None:
- raise LodelFatalError("Calling a Client classmethod but no Client \
- instance exists")
- return cls._instance
-
- ##@brief Alias of Client::destroy()
- @classmethod
- def deauth(cls):
- cls.destroy()
-
- ##@brief Destroy current client
- @classmethod
- def destroy(cls):
- inst = cls._instance
- cls._instance = None
- del(inst)
-
- ##@brief Authenticate using login an password
- #@note Wrapper on Auth.auth()
- #@param login str
- #@param password str
- #@return None
- #@throw AuthenticationFailure
- #@see Auth.auth()
- @classmethod
- def auth_password(cls, login, password):
- cls.client.auth(login, password)
-
- ##@brief Authenticate using session token
- #@param token str : session token
- #@throw AuthenticationFailure
- #@see Auth.auth_session()
- @classmethod
- def auth_session(cls, token):
- cls.client.auth_session(token)
-
- ##@brief Generic authentication method
- #
- #Possible arguments are :
- # - authenticate(token) ( see @ref Client.auth_session() )
- # - authenticate(login, password) ( see @ref Client.auth_password() )
- #@param *args
- #@param **kwargs
- @classmethod
- def authenticate(cls, *args, **kwargs):
- token = None
- login_pass = None
- if 'token' in kwargs:
- #auth session
- if len(args) != 0 or len(kwargs) != 0:
- # security issue ?
- raise AuthenticationSecurityError(cls.client())
- else:
- session = kwargs['token']
- elif len(args) == 1:
- if len(kwargs) == 0:
- #Auth session
- token = args[0]
- elif len(kwargs) == 1:
- if 'login' in kwargs:
- login_pass = (kwargs['login'], args[0])
- elif 'password' in kwargs:
- login_pass = (args[0], kwargs['password'])
- elif len(args) == 2:
- login_pass = tuple(args)
-
- if login_pass is None and token is None:
- # bad arguments given. Security issue ?
- raise AuthenticationSecurityError(cls.client())
- elif login_pass is None:
- cls.auth_session(token)
- else:
- cls.auth_password(*login_pass)
-
-
- ##@brief Singleton class that handles authentication on lodel2 instances
- #
- #
- #@note Designed to be never called directly. The Client class is designed to
- #be implemented by UI and to provide a friendly/secure API for \
- #client/auth/session handling
- #@todo specs of client infos given as argument on authentication methods
- class Auth(object):
-
- ##@brief Stores 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 Constructor
- #
- #@note Automatic clean of previous instance
- def __init__(self, client):
- ##@brief Stores infos about logged in user
- #
- #Tuple containing (LeObjectChild, UID) of logged in user
- self.__user_infos = False
- ##@brief Stores session id
- self.__session_id = False
- if not isinstance(client, Client):
- msg = "<class Client> instance was expected but got %s"
- msg %= type(client)
- raise TypeError(msg)
- ##@brief Stores client infos
- self.__client = client
-
- # Singleton
- if self._instance is not None:
- bck = self._instance
- bck.destroy()
- self._instance = None
- logger.debug("Previous Auth instance replaced by a new one")
- else:
- #First instance, fetching settings
- self.fetch_settings()
- self.__class__._instance = self
-
- ##@brief Destroy current instance an associated session
- def _destroy(self):
- self.__user_infos = LodelHook.call_hook('lodel2_session_destroy',
- caller = self, payload = self.__session_id)
-
- ##@brief Destroy singleton instance
- @classmethod
- def destroy(cls):
- cls._instance._destroy()
-
- ##@brief Raise exception because of authentication failure
- #@note trigger a security log containing client infos
- #@throw LodelFatalError if no instance exsists
- #@see Auth.fail()
- @classmethod
- def failure(cls):
- if cls._instance is None:
- raise LodelFatalError("No Auth instance found. Abording")
- raise AuthenticationFailure(cls._instance.fail())
-
- ##@brief Class method that fetches conf
- @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 Raise an AuthenticationFailure exception
- #
- #@note Trigger a security log message containing client infos
- def fail(self):
- raise AuthenticationFailure(self.__client)
-
- ##@brief Is the user anonymous ?
- #@return True if no one is logged in
- def is_anon(self):
- return self._login is False
-
- ##@brief Authenticate using a login and a password
- #@param login str : provided login
- #@param password str : provided password
- #@todo automatic hashing
- #@warning brokes multiple UID
- #@note implements multiple login/password sources (useless ?)
- #@todo composed UID broken in this method
- def auth(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 Authenticate using a session token
- #@note Call a dedicated hook in order to allow session implementation as
- #plugin
- #@thrown AuthenticationFailure
- def auth_session(self, token):
- try:
- self.__user_infos = LodelHook.call_hook('lodel2_session_load',
- caller = self, payload = token)
- except AuthenticationError:
- self.fail() #Security logging
- self.__session_id = token
-
- ##@brief Set a user as authenticated and start a new session
- #@param leo LeObject child class : The EmClass the user belong to
- #@param uid str : uniq ID (in leo)
- #@return None
- def __set_authenticated(self, leo, uid):
- # Storing user infos
- self.__user_infos = {'classname': leo.__name__, 'uid': uid}
- # Init session
- sid = LodelHook.call_hook('lodel2_session_start', caller = self,
- payload = copy.copy(self.__user_infos))
- self.__session_id = sid
|