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

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