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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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. cls._instance.__datas = SessionHandler.restore(token)
  143. cls._instance.__session_token = token
  144. return copy.copy(cls._instance.datas)
  145. ##@brief Return the current session token or None
  146. #@return A session token or None
  147. @classmethod
  148. def session_token(cls):
  149. cls._assert_instance()
  150. return cls._instance.__session_token
  151. ##@brief Delete current session
  152. @classmethod
  153. def destroy(cls):
  154. cls._assert_instance()
  155. SessionHandler.destroy(cls._instance.__session_token)
  156. cls._instance.__session_token = None
  157. cls._instance.__datas = dict()
  158. ##@brief Delete current client and save its session
  159. @classmethod
  160. def clean(cls):
  161. if cls._instance.__session_token is not None:
  162. SessionHandler.save(cls._instance.__session_token, cls._instance.__datas)
  163. if Client._instance is not None:
  164. del(Client._instance)
  165. Client._instance = None
  166. ##@brief Test wether a client is anonymous or logged in
  167. #@return True if client is anonymous
  168. @classmethod
  169. def is_anonymous(cls):
  170. return Client._instance.user() is None
  171. ##@brief Method to call on authentication failure
  172. #@throw ClientAuthenticationFailure
  173. #@throw LodelFatalError if no Client child instance found
  174. @classmethod
  175. def authentication_failure(cls):
  176. cls._generic_error(ClientAuthenticationFailure)
  177. ##@brief Method to call on authentication error
  178. #@throw ClientAuthenticationError
  179. #@throw LodelFatalError if no Client child instance found
  180. @classmethod
  181. def authentication_error(cls, msg = "Unknow error"):
  182. cls._generic_error(ClientAuthenticationError, msg)
  183. ##@brief Method to call on permission denied error
  184. #@throw ClientPermissionDenied
  185. #@throw LodelFatalError if no Client child instance found
  186. @classmethod
  187. def permission_denied_error(cls, msg = ""):
  188. cls._generic_error(ClientPermissionDenied, msg)
  189. ##@brief Generic error method
  190. #@see Client::authentication_failure() Client::authentication_error()
  191. #Client::permission_denied_error()
  192. #@throw LodelFatalError if no Client child instance found
  193. @classmethod
  194. def _generic_error(cls, expt, msg = ""):
  195. cls._assert_instance()
  196. raise expt(Client._instance, msg)
  197. ##@brief Assert that an instance of Client child class exists
  198. #@throw LodelFataError if no instance of Client child class found
  199. @classmethod
  200. def _assert_instance(cls):
  201. if Client._instance is None:
  202. raise LodelFatalError("No client instance found. Abording.")
  203. ##@brief Class method that fetches conf
  204. #
  205. #This method populates Client._infos_fields . This attribute stores
  206. #informations on login and password location (LeApi object & field)
  207. @classmethod
  208. def fetch_settings(cls):
  209. from lodel import dyncode
  210. if cls._infos_fields is None:
  211. cls._infos_fields = list()
  212. else:
  213. #Allready fetched
  214. return
  215. infos = (
  216. Settings.auth.login_classfield,
  217. Settings.auth.pass_classfield)
  218. res_infos = []
  219. for clsname, fieldname in infos:
  220. dcls = dyncode.lowername2class(infos[0][0])
  221. res_infos.append((dcls, infos[1][1]))
  222. link_field = None
  223. if res_infos[0][0] != res_infos[1][0]:
  224. # login and password are in two separated EmClass
  225. # determining the field that links login EmClass to password
  226. # EmClass
  227. for fname, fdh in res_infos[0][0].fields(True).items():
  228. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  229. link_field = fname
  230. if link_field is None:
  231. #Unable to find link between login & password EmClasses
  232. raise AuthenticationError("Unable to find a link between \
  233. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  234. res_infos[0][0], res_infos[1][0]))
  235. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  236. cls._infos_fields.append(
  237. {'login':res_infos[0], 'password':res_infos[1]})
  238. ##@brief Set a user as authenticated and start a new session
  239. #@param leo LeObject child class : the LeObject the user is stored in
  240. #@param uid str : uniq id (in leo)
  241. #@return None
  242. @classmethod
  243. def __set_authenticated(cls, leo, uid):
  244. cls._instance.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}
  245. #Store auth infos in session
  246. cls._instance.__datas[cls._instance.__class__._AUTH_DATANAME] = copy.copy(cls._instance.__user)