mirror of
				https://github.com/yweber/lodel2.git
				synced 2025-10-31 11:39:02 +01:00 
			
		
		
		
	Authentication handlers implementation
- implements a Client abstract singleton class designed to be implemented by UI to be able to register clients informations - implements a Auth singleton class. Kind of interface between Client singleton and session hanlder plugin
This commit is contained in:
		
					parent
					
						
							
								92c12150f2
							
						
					
				
			
			
				commit
				
					
						82f12d95ec
					
				
			
		
					 3 changed files with 333 additions and 2 deletions
				
			
		|  | @ -1 +1,27 @@ | |||
| __author__ = 'roland' | ||||
| ##@package lodel.auth Package handling authentication on Lodel2 | ||||
| # | ||||
| #The authentication mechanism are divided in multiple peaces :  | ||||
| #- The client ( @ref lodel.auth.auth.Client ) singleton class that stores | ||||
| #clients infos | ||||
| #- The @ref lodel.auth.Auth class handles authentication, sessions | ||||
| #creation/load/deletion | ||||
| #- The session handler implement as a plugin | ||||
| # | ||||
| #@par Client class | ||||
| # | ||||
| #The @ref lodel.auth.auth.Client class is an abstract singleton. It is designed | ||||
| #to be implemented by UI plugins. In fact we don't have the same client | ||||
| #informations on a web UI, on a CLI or with UDP communications. The main goal | ||||
| #of this class is to provide an API to interface plugins to stores client | ||||
| #informations allowing lodel2 to produce security log messages containing  | ||||
| #client informations. | ||||
| # | ||||
| #@par Auth class | ||||
| # | ||||
| #The auth class is a singleton designed to actually do authentication. | ||||
| #This class fetch from settings the Emclass and it's field that contains | ||||
| #login and password. It's also an API between Client class and session handler | ||||
| # | ||||
| #@par Session handler | ||||
| # | ||||
| #Implemented as a plugin, called with hooks. | ||||
|  |  | |||
							
								
								
									
										291
									
								
								lodel/auth/auth.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								lodel/auth/auth.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,291 @@ | |||
| #-*- 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 | ||||
| 
 | ||||
|         # 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 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(self) | ||||
|             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 is None: | ||||
|             cls.auth_session(token) | ||||
|         else: | ||||
|             cls.auth_passwor(*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 | ||||
|             self._instance = None | ||||
|             del(bck) | ||||
|             logger.debug("Previous Auth instance replaced by a new one") | ||||
|         else: | ||||
|             #First instance, fetching settings | ||||
|             self.fetch_settings() | ||||
|         self.__class__._instance = self | ||||
|          | ||||
|     ##@brief Instance destructor | ||||
|     def __del__(self): | ||||
|         self.__user_infos = LodelHook.call_hook('lodel2_session_destroy', | ||||
|             caller = self, payload = token) | ||||
|         pass | ||||
|      | ||||
|     ##@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(self): | ||||
|         from lodel import dyncode | ||||
|         if cls._infos_fields is None: | ||||
|             cls._infos_fields = list() | ||||
|         else: | ||||
|             #Allready fetched | ||||
|             return | ||||
|         infos = ( | ||||
|             Settings.auth.login_classfield.split('.'), | ||||
|             Settings.auth.pass_classfield.split('.')) | ||||
|         res_infos = [] | ||||
|         for clsname, fieldname in infos: | ||||
|             res_infos.append(( | ||||
|                 dyncode.lowername2class(infos[0]), | ||||
|                 dcls.field(infos[1]))) | ||||
| 
 | ||||
|         link_field = None | ||||
|         if res_infos[0][0] != res_infos[0][1]: | ||||
|             # 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): | ||||
|                 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 | ||||
| 
 | ||||
|  | @ -1,12 +1,26 @@ | |||
| from lodel import logger | ||||
| 
 | ||||
| ##@brief Handles common errors on authentication | ||||
| class AuthenticationError(Exception): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| ##@brief Handles authentication error with possible security issue | ||||
| # | ||||
| #@note Handle the creation of a security log message containing client info | ||||
| class AuthenticationSecurityError(AuthenticationError): | ||||
|     def __init__(self, client): | ||||
|         msg = "%s : authentication error" % client | ||||
|         logger.security(msg) | ||||
|         super().__init__(msg) | ||||
| 
 | ||||
| 
 | ||||
| ##@brief Handles authentication failure | ||||
| # | ||||
| #@note Handle the creation of a security log message containing client info | ||||
| class AuthenticationFailure(Exception): | ||||
|      | ||||
|     def __init__(self, client): | ||||
|         msg = "%s : authentication failure" % client | ||||
|         logger.security(msg) | ||||
|         super().__init__(msg) | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Yann
				Yann