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 13KB

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