Няма описание
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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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 Class designed to handle sessions and its datas
  13. class LodelSession(object):
  14. ##@brief Try to restore or to create a session
  15. #@param token None | mixed : If None no session will be loaded nor created
  16. #restore an existing session
  17. #@throw ClientAuthenticationFailure if a session restore fails
  18. def __init__(self, token = None):
  19. ##@brief Stores the session token
  20. self.__token = token
  21. ##@brief Stores the session datas
  22. self.__datas = dict()
  23. if token is not None:
  24. self.restore(token)
  25. ##@brief A token reference checker to ensure token deletion at the end of
  26. #a session
  27. #@warning Cause maybe a performance issue !
  28. def __token_checker(self):
  29. refcount = sys.getrefcount(self.__token)
  30. if self.__token is not None and refcount > 1:
  31. warnings.warn("More than one reference to the session token \
  32. exists ! (exactly %d)" % refcount)
  33. ##@brief Property. Equals True if a session is started else False
  34. @property
  35. def started(self):
  36. res = self.__token is not None
  37. if res:
  38. self.__token_checker()
  39. return res
  40. ##@brief Property that ensure ro acces to sessions datas
  41. @property
  42. def datas(self):
  43. return copy.copy(self.__datas)
  44. ##@brief Return the session token
  45. def retrieve_token(self):
  46. # DO NOT COPY THE TOKEN TO ENSURE THAT NOT MORE THAN ONE REFERENCE TO
  47. # IT EXISTS
  48. return self.__token
  49. ##@brief Late restore of a session
  50. #@param token mixed : the session token
  51. #@throw ClientAuthenticationError if a session was allready started
  52. #@throw ClientAuthenticationFailure if no session exists with this token
  53. def restore(self, token):
  54. if self.started:
  55. raise ClientAuthenticationError("Trying to restore a session, but \
  56. a session is allready started !!!")
  57. self.__datas = SessionHandler.restore(token)
  58. self.__token = token
  59. return self.datas
  60. ##@brief Save the current session state
  61. def save(self):
  62. if not self.started:
  63. raise ClientAuthenticationError(
  64. "Trying to save a non started session")
  65. SessionHandler.save(self.__token, self.__datas)
  66. ##@brief Destroy a session
  67. def destroy(self):
  68. if not self.started:
  69. logger.debug("Destroying a session that is not started")
  70. else:
  71. SessionHandler.destroy(self.__token)
  72. self.__token = None
  73. self.__datas = dict()
  74. ##@brief Destructor
  75. def __del__(self):
  76. del(self.__token)
  77. del(self.__datas)
  78. ##@brief Implements setter for dict access to instance
  79. #@todo Custom exception throwing
  80. def __setitem__(self, key, value):
  81. self.__init_session() #Start the sesssion
  82. self.__datas[key] = value
  83. ##@brief Implements destructor for dict access to instance
  84. #@todo Custom exception throwing
  85. def __delitem__(self, key):
  86. if not self.started:
  87. raise ClientAuthenticationError(
  88. "Data read access to a non started session is not possible")
  89. del(self.__datas[key])
  90. ##@brief Implements getter for dict acces to instance
  91. #@todo Custom exception throwing
  92. def __getitem__(self, key):
  93. if not self.started:
  94. raise ClientAuthenticationError(
  95. "Data read access to a non started session is not possible")
  96. return self.__datas[key]
  97. ##@brief Start a new session
  98. #@note start a new session only if no session started yet
  99. def __init_session(self):
  100. if self.__token is not None:
  101. return
  102. self.__token = SessionHandler.start()
  103. ##@brief Client metaclass designed to implements container accessor on
  104. #Client Class
  105. #
  106. #@todo Maybe we can delete this metaclass....
  107. class ClientMetaclass(type):
  108. def __init__(self, name, bases, attrs):
  109. return super(ClientMetaclass, self).__init__(name, bases, attrs)
  110. def __getitem__(self, key):
  111. return self.session()[key]
  112. def __delitem__(self, key):
  113. del(self.session()[key])
  114. def __setitem__(self, key, value):
  115. self.session()[key] = value
  116. def __str__(self):
  117. return str(self._instance)
  118. ##@brief Abstract singleton class designed to handle client informations
  119. #
  120. # This class is designed to handle client authentication and sessions
  121. class Client(object, metaclass = ClientMetaclass):
  122. ##@brief Singleton instance
  123. _instance = None
  124. ##@brief List of dict that stores field ref for login and password
  125. #
  126. # Storage specs :
  127. #
  128. # A list of dict, with keys 'login' and 'password', items are tuple.
  129. #- login tuple contains (LeObjectChild, FieldName, link_field) with:
  130. # - LeObjectChild the dynclass containing the login
  131. # - Fieldname the fieldname of LeObjectChild containing the login
  132. # - link_field None if both login and password are in the same
  133. # LeObjectChild. Else contains the field that make the link between
  134. # login LeObject and password LeObject
  135. #- password typle contains (LeObjectChild, FieldName)
  136. _infos_fields = None
  137. ##@brief Constant that stores the session key that stores authentication
  138. #informations
  139. _AUTH_DATANAME = '__auth_user_infos'
  140. ##@brief Constructor
  141. #@param session_token mixed : Session token provided by client to interface
  142. def __init__(self,session_token = None):
  143. if self.__class__ == Client:
  144. raise NotImplementedError("Abstract class")
  145. logger.debug("New instance of Client child class %s" %
  146. self.__class__.__name__)
  147. if Client._instance is not None:
  148. old = Client._instance
  149. Client._instance = None
  150. del(old)
  151. logger.debug("Replacing old Client instance by a new one")
  152. else:
  153. #first instanciation, fetching settings
  154. self.fetch_settings()
  155. ##@brief Stores infos for authenticated users (None == anonymous)
  156. self.__user = None
  157. ##@brief Stores the session handler
  158. Client._instance = self
  159. ##@brief Stores LodelSession instance
  160. self.__session = LodelSession(session_token)
  161. logger.debug("New client : %s" % self)
  162. def __del__(self):
  163. del(self.__session)
  164. ##@brief Try to authenticate a user with a login and a password
  165. #@param login str : provided login
  166. #@param password str : provided password (hash)
  167. #@warning brokes composed UID
  168. #@note implemets multiple login/password sources (useless ?)
  169. #@todo composed UID broken method
  170. #@todo allow to provide an authentication source
  171. @classmethod
  172. def authenticate(self, login = None, password = None):
  173. #Authenticate
  174. for infos in self._infos_fields:
  175. logger.debug(self._infos_fields)
  176. login_cls = infos['login'][0]
  177. pass_cls = infos['password'][0]
  178. qfilter = "{passfname} = {passhash}"
  179. uid_fname = login_cls.uid_fieldname()[0] #COMPOSED UID BROKEN
  180. if login_cls == pass_cls:
  181. #Same EmClass for login & pass
  182. qfilter = qfilter.format(
  183. passfname = infos['password'][1],
  184. passhash = password)
  185. else:
  186. #Different EmClass, building a relational filter
  187. passfname = "%s.%s" % (infos['login'][2], infos['password'][1])
  188. qfilter = qfilter.format(
  189. passfname = passfname,
  190. passhash = password)
  191. getq = LeGetQuery(infos['login'][0], qfilter,
  192. field_list = [uid_fname], limit = 1)
  193. req = getq.execute()
  194. if len(req) == 1:
  195. self.__set_authenticated(infos['login'][0],req[0][uid_fname])
  196. break
  197. if self.is_anonymous():
  198. self.authentication_failure() #Security logging
  199. ##@brief Attempt to restore a session given a session token
  200. #@param token mixed : a session token
  201. #@return Session datas (a dict)
  202. #@throw ClientAuthenticationFailure if token is not valid or not
  203. #existing
  204. @classmethod
  205. def restore_session(cls, token):
  206. cls._assert_instance()
  207. return Client._instance.__session.restore(token)
  208. ##@brief Return the current session token or None
  209. #@return A session token or None
  210. @classmethod
  211. def session_token(cls):
  212. cls._assert_instance()
  213. return Client._instance.__session.retrieve_token()
  214. @classmethod
  215. def session(cls):
  216. cls._assert_instance()
  217. return Client._instance.__session
  218. ##@brief Delete current session
  219. @classmethod
  220. def destroy(cls):
  221. cls._assert_instance()
  222. Client._instance.__session.destroy()
  223. ##@brief Delete current client and save its session
  224. @classmethod
  225. def clean(cls):
  226. if Client._instance.__session.started:
  227. Client._instance.__session.save()
  228. if Client._instance is not None:
  229. del(Client._instance)
  230. Client._instance = None
  231. ##@brief Test wether a client is anonymous or logged in
  232. #@return True if client is anonymous
  233. @classmethod
  234. def is_anonymous(cls):
  235. return cls._assert_instance()
  236. #return Client._instance
  237. ##@brief Test wether a client is guest or logged in
  238. #@return True if client is anonymous
  239. #@ TODO : to be improved
  240. @classmethod
  241. def is_guest(cls):
  242. return len(cls._instance.__session.datas) == 1
  243. ##@brief Method to call on authentication failure
  244. #@throw ClientAuthenticationFailure
  245. #@throw LodelFatalError if no Client child instance found
  246. @classmethod
  247. def authentication_failure(cls):
  248. cls._generic_error(ClientAuthenticationFailure)
  249. ##@brief Method to call on authentication error
  250. #@throw ClientAuthenticationError
  251. #@throw LodelFatalError if no Client child instance found
  252. @classmethod
  253. def authentication_error(cls, msg = "Unknow error"):
  254. cls._generic_error(ClientAuthenticationError, msg)
  255. ##@brief Method to call on permission denied error
  256. #@throw ClientPermissionDenied
  257. #@throw LodelFatalError if no Client child instance found
  258. @classmethod
  259. def permission_denied_error(cls, msg = ""):
  260. cls._generic_error(ClientPermissionDenied, msg)
  261. ##@brief Generic error method
  262. #@see Client::authentication_failure() Client::authentication_error()
  263. #Client::permission_denied_error()
  264. #@throw LodelFatalError if no Client child instance found
  265. @classmethod
  266. def _generic_error(cls, expt, msg = ""):
  267. cls._assert_instance()
  268. raise expt(Client._instance, msg)
  269. ##@brief Assert that an instance of Client child class exists
  270. #@throw LodelFataError if no instance of Client child class found
  271. @classmethod
  272. def _assert_instance(cls):
  273. if Client._instance is None:
  274. raise LodelFatalError("No client instance found. Abording.")
  275. ##@brief Class method that fetches conf
  276. #
  277. #This method populates Client._infos_fields . This attribute stores
  278. #informations on login and password location (LeApi object & field)
  279. @classmethod
  280. def fetch_settings(cls):
  281. from lodel import dyncode
  282. if cls._infos_fields is None:
  283. cls._infos_fields = list()
  284. else:
  285. #Allready fetched
  286. return
  287. infos = (
  288. Settings.auth.login_classfield,
  289. Settings.auth.pass_classfield)
  290. res_infos = []
  291. for clsname, fieldname in infos:
  292. dcls = dyncode.lowername2class(infos[0][0])
  293. res_infos.append((dcls, infos[1][1]))
  294. link_field = None
  295. if res_infos[0][0] != res_infos[1][0]:
  296. # login and password are in two separated EmClass
  297. # determining the field that links login EmClass to password
  298. # EmClass
  299. for fname, fdh in res_infos[0][0].fields(True).items():
  300. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  301. link_field = fname
  302. if link_field is None:
  303. #Unable to find link between login & password EmClasses
  304. raise AuthenticationError("Unable to find a link between \
  305. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  306. res_infos[0][0], res_infos[1][0]))
  307. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  308. cls._infos_fields.append(
  309. {'login':res_infos[0], 'password':res_infos[1]})
  310. ##@brief Set a user as authenticated and start a new session
  311. #@param leo LeObject child class : the LeObject the user is stored in
  312. #@param uid str : uniq id (in leo)
  313. #@return None
  314. @classmethod
  315. def __set_authenticated(self, leo, uid):
  316. self.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}
  317. #Store auth infos in session
  318. self._instance.__session[self._instance.__class__._AUTH_DATANAME] = copy.copy(self.__user)