No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #-*- Coding: utf-8 -*-
  2. import copy
  3. import sys
  4. import warnings
  5. import inspect
  6. from lodel.settings import Settings
  7. from lodel import logger
  8. from lodel.plugin.hooks import LodelHook
  9. from lodel.plugin import SessionHandlerPlugin as SessionHandler
  10. from .exceptions import *
  11. from ..leapi.query import LeGetQuery
  12. ##@brief Client metaclass designed to implements container accessor on
  13. #Client Class
  14. #
  15. #@todo Maybe we can delete this metaclass....
  16. class ClientMetaclass(type):
  17. def __init__(self, name, bases, attrs):
  18. return super(ClientMetaclass, self).__init__(name, bases, attrs)
  19. def __getitem__(self, key):
  20. return self.datas()[key]
  21. def __delitem__(self, key):
  22. del(self.datas()[key])
  23. def __setitem__(self, key, value):
  24. if self.get_session_token() is None:
  25. self.set_session_token(SessionHandler.start())
  26. datas = self.datas()
  27. datas[key] = value
  28. def __str__(self):
  29. return str(self._instance)
  30. ##@brief Abstract singleton class designed to handle client informations
  31. #
  32. # This class is designed to handle client authentication and sessions
  33. class Client(object, metaclass = ClientMetaclass):
  34. ##@brief Singleton instance
  35. _instance = None
  36. ##@brief List of dict that stores field ref for login and password
  37. #
  38. # Storage specs :
  39. #
  40. # A list of dict, with keys 'login' and 'password', items are tuple.
  41. #- login tuple contains (LeObjectChild, FieldName, link_field) with:
  42. # - LeObjectChild the dynclass containing the login
  43. # - Fieldname the fieldname of LeObjectChild containing the login
  44. # - link_field None if both login and password are in the same
  45. # LeObjectChild. Else contains the field that make the link between
  46. # login LeObject and password LeObject
  47. #- password typle contains (LeObjectChild, FieldName)
  48. _infos_fields = None
  49. ##@brief Constant that stores the session key that stores authentication
  50. #informations
  51. _AUTH_DATANAME = '__auth_user_infos'
  52. ##@brief Constructor
  53. #@param session_token mixed : Session token provided by client to interface
  54. def __init__(self,session_token = None):
  55. logger.debug(session_token)
  56. if self.__class__ == Client:
  57. raise NotImplementedError("Abstract class")
  58. logger.debug("New instance of Client child class %s" %
  59. self.__class__.__name__)
  60. if Client._instance is not None:
  61. old = Client._instance
  62. Client._instance = None
  63. del(old)
  64. logger.debug("Replacing old Client instance by a new one")
  65. else:
  66. #first instanciation, fetching settings
  67. self.fetch_settings()
  68. ##@brief Stores infos for authenticated users (None == anonymous)
  69. self.__user = None
  70. ##@brief Stores the session handler
  71. Client._instance = self
  72. ##@brief Stores LodelSession instance
  73. self.__datas = dict()
  74. if session_token is not None:
  75. self.__datas = SessionHandler.restore(session_token)
  76. self.__session_token = session_token
  77. logger.debug("New client : %s" % self)
  78. def __del__(self):
  79. del(self.__session_token)
  80. del(self.__datas)
  81. @classmethod
  82. def datas(cls):
  83. return cls._instance.__datas
  84. @classmethod
  85. def user(cls):
  86. if '__auth_user_infos' in cls._instance.__datas:
  87. return cls._instance.__datas['__auth_user_infos']
  88. else:
  89. return None
  90. @classmethod
  91. def get_session_token(cls):
  92. return cls._instance.__session_token
  93. @classmethod
  94. def set_session_token(cls, value):
  95. cls._instance.__session_token = value
  96. ##@brief Try to authenticate a user with a login and a password
  97. #@param login str : provided login
  98. #@param password str : provided password (hash)
  99. #@warning brokes composed UID
  100. #@note implemets multiple login/password sources (useless ?)
  101. #@todo composed UID broken method
  102. #@todo allow to provide an authentication source
  103. @classmethod
  104. def authenticate(self, login = None, password = None):
  105. #Authenticate
  106. for infos in self._infos_fields:
  107. logger.debug(self._infos_fields)
  108. login_cls = infos['login'][0]
  109. pass_cls = infos['password'][0]
  110. qfilter = "{passfname} = {passhash}"
  111. uid_fname = login_cls.uid_fieldname()[0] #COMPOSED UID BROKEN
  112. if login_cls == pass_cls:
  113. #Same EmClass for login & pass
  114. qfilter = qfilter.format(
  115. passfname = infos['password'][1],
  116. passhash = password)
  117. else:
  118. #Different EmClass, building a relational filter
  119. passfname = "%s.%s" % (infos['login'][2], infos['password'][1])
  120. qfilter = qfilter.format(
  121. passfname = passfname,
  122. passhash = password)
  123. getq = LeGetQuery(infos['login'][0], qfilter,
  124. field_list = [uid_fname], limit = 1)
  125. req = getq.execute()
  126. if len(req) == 1:
  127. self.__set_authenticated(infos['login'][0],req[0][uid_fname])
  128. break
  129. if self.is_anonymous():
  130. self.authentication_failure() #Security logging
  131. ##@brief Attempt to restore a session given a session token
  132. #@param token mixed : a session token
  133. #@return Session datas (a dict)
  134. #@throw ClientAuthenticationFailure if token is not valid or not
  135. #existing
  136. @classmethod
  137. def restore_session(cls, token):
  138. cls._assert_instance()
  139. if cls._instance.__session_token is not None:
  140. raise ClientAuthenticationError("Trying to restore a session, but \
  141. a session is allready started !!!")
  142. try:
  143. cls._instance.__datas = SessionHandler.restore(token)
  144. cls._instance.__session_token = token
  145. except ClientAuthenticationFailure:
  146. logger.warning("Session restoring fails")
  147. return copy.copy(cls._instance.datas)
  148. ##@brief Return the current session token or None
  149. #@return A session token or None
  150. @classmethod
  151. def session_token(cls):
  152. cls._assert_instance()
  153. return cls._instance.__session_token
  154. ##@brief Delete current session
  155. @classmethod
  156. def destroy(cls):
  157. cls._assert_instance()
  158. SessionHandler.destroy(cls._instance.__session_token)
  159. cls._instance.__session_token = None
  160. cls._instance.__datas = dict()
  161. ##@brief Delete current client and save its session
  162. @classmethod
  163. def clean(cls):
  164. if cls._instance.__session_token is not None:
  165. SessionHandler.save(cls._instance.__session_token, cls._instance.__datas)
  166. if Client._instance is not None:
  167. del(Client._instance)
  168. Client._instance = None
  169. ##@brief Test wether a client is anonymous or logged in
  170. #@return True if client is anonymous
  171. @classmethod
  172. def is_anonymous(cls):
  173. return Client._instance.user() is None
  174. ##@brief Method to call on authentication failure
  175. #@throw ClientAuthenticationFailure
  176. #@throw LodelFatalError if no Client child instance found
  177. @classmethod
  178. def authentication_failure(cls):
  179. cls._generic_error(ClientAuthenticationFailure)
  180. ##@brief Method to call on authentication error
  181. #@throw ClientAuthenticationError
  182. #@throw LodelFatalError if no Client child instance found
  183. @classmethod
  184. def authentication_error(cls, msg = "Unknow error"):
  185. cls._generic_error(ClientAuthenticationError, msg)
  186. ##@brief Method to call on permission denied error
  187. #@throw ClientPermissionDenied
  188. #@throw LodelFatalError if no Client child instance found
  189. @classmethod
  190. def permission_denied_error(cls, msg = ""):
  191. cls._generic_error(ClientPermissionDenied, msg)
  192. ##@brief Generic error method
  193. #@see Client::authentication_failure() Client::authentication_error()
  194. #Client::permission_denied_error()
  195. #@throw LodelFatalError if no Client child instance found
  196. @classmethod
  197. def _generic_error(cls, expt, msg = ""):
  198. cls._assert_instance()
  199. raise expt(Client._instance, msg)
  200. ##@brief Assert that an instance of Client child class exists
  201. #@throw LodelFataError if no instance of Client child class found
  202. @classmethod
  203. def _assert_instance(cls):
  204. if Client._instance is None:
  205. raise LodelFatalError("No client instance found. Abording.")
  206. ##@brief Class method that fetches conf
  207. #
  208. #This method populates Client._infos_fields . This attribute stores
  209. #informations on login and password location (LeApi object & field)
  210. @classmethod
  211. def fetch_settings(cls):
  212. from lodel import dyncode
  213. if cls._infos_fields is None:
  214. cls._infos_fields = list()
  215. else:
  216. #Allready fetched
  217. return
  218. infos = (
  219. Settings.auth.login_classfield,
  220. Settings.auth.pass_classfield)
  221. res_infos = []
  222. for clsname, fieldname in infos:
  223. dcls = dyncode.lowername2class(infos[0][0])
  224. res_infos.append((dcls, infos[1][1]))
  225. link_field = None
  226. if res_infos[0][0] != res_infos[1][0]:
  227. # login and password are in two separated EmClass
  228. # determining the field that links login EmClass to password
  229. # EmClass
  230. for fname, fdh in res_infos[0][0].fields(True).items():
  231. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  232. link_field = fname
  233. if link_field is None:
  234. #Unable to find link between login & password EmClasses
  235. raise AuthenticationError("Unable to find a link between \
  236. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  237. res_infos[0][0], res_infos[1][0]))
  238. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  239. cls._infos_fields.append(
  240. {'login':res_infos[0], 'password':res_infos[1]})
  241. ##@brief Set a user as authenticated and start a new session
  242. #@param leo LeObject child class : the LeObject the user is stored in
  243. #@param uid str : uniq id (in leo)
  244. #@return None
  245. @classmethod
  246. def __set_authenticated(cls, leo, uid):
  247. cls._instance.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}
  248. #Store auth infos in session
  249. cls._instance.__datas[cls._instance.__class__._AUTH_DATANAME] = copy.copy(cls._instance.__user)