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

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