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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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.logger import logger,
  8. from lodel.plugin import SessionHandlerPlugin, SessionHandler
  9. from lodel.auth.exceptions import ClientError, ClientAuthenticationFailure, ClientPermissionDenied, ClientAuthenticationError
  10. from lodel.leapi.query import LeGetQuery
  11. ## @brief Client metaclass designed to implements container accessor on
  12. # Client Class
  13. #
  14. #@todo Maybe we can delete this metaclass....
  15. class ClientMetaclass(type):
  16. def __init__(self, name, bases, attrs):
  17. return super(ClientMetaclass, self).__init__(name, bases, attrs)
  18. def __getitem__(self, key):
  19. return self.data()[key]
  20. def __delitem__(self, key):
  21. del(self.data()[key])
  22. def __setitem__(self, key, value):
  23. if self.get_session_token() is None:
  24. self.set_session_token(SessionHandler.start())
  25. data = self.data()
  26. data[key] = value
  27. def __str__(self):
  28. return str(self._instance)
  29. ## @brief Abstract singleton class designed to handle client informations
  30. #
  31. # This class is designed to handle client authentication and sessions
  32. class Client(object, metaclass=ClientMetaclass):
  33. ## @brief Singleton instance
  34. _instance = None
  35. # @brief List of dict that stores field ref for login and password
  36. #
  37. # Storage specs :
  38. #
  39. # A list of dict, with keys 'login' and 'password', items are tuple.
  40. # - login tuple contains (LeObjectChild, FieldName, link_field) with:
  41. # - LeObjectChild the dynclass containing the login
  42. # - Fieldname the fieldname of LeObjectChild containing the login
  43. # - link_field None if both login and password are in the same
  44. # LeObjectChild. Else contains the field that make the link between
  45. # login LeObject and password LeObject
  46. # - password typle contains (LeObjectChild, FieldName)
  47. _infos_fields = None
  48. ## @brief Constant that stores the session key that stores authentication
  49. # informations
  50. _AUTH_DATANAME = '__auth_user_infos'
  51. ## @brief Constructor
  52. #@param session_token mixed : Session token provided by client to interface
  53. def __init__(self, session_token=None):
  54. logger.debug(session_token)
  55. if self.__class__ == Client:
  56. raise NotImplementedError("Abstract class")
  57. logger.debug("New instance of Client child class %s" %
  58. self.__class__.__name__)
  59. if Client._instance is not None:
  60. old = Client._instance
  61. Client._instance = None
  62. del(old)
  63. logger.debug("Replacing old Client instance by a new one")
  64. else:
  65. ## first instanciation, fetching settings
  66. self.fetch_settings()
  67. # @brief Stores infos for authenticated users (None == anonymous)
  68. self.__user = None
  69. ## @brief Stores the session handler
  70. Client._instance = self
  71. ## @brief Stores LodelSession instance
  72. self.__data = dict()
  73. if session_token is not None:
  74. self.__data = SessionHandler.restore(session_token)
  75. self.__session_token = session_token
  76. logger.debug("New client : %s" % self)
  77. def __del__(self):
  78. del(self.__session_token)
  79. del(self.__data)
  80. ## @brief Returns session
  81. #@ returns the dict which stores session
  82. @classmethod
  83. def data(cls):
  84. return cls._instance.__data
  85. ## @brief Returns the user's information contained in the session's data
  86. @classmethod
  87. def user(cls):
  88. if '__auth_user_infos' in cls._instance.__data:
  89. return cls._instance.__data['__auth_user_infos']
  90. else:
  91. return None
  92. ## @brief Returns the session's token
  93. @classmethod
  94. def get_session_token(cls):
  95. return cls._instance.__session_token
  96. ## @brief Set the session's token
  97. #@param the value of the token
  98. @classmethod
  99. def set_session_token(cls, value):
  100. cls._instance.__session_token = value
  101. ## @brief Try to authenticate a user with a login and a password
  102. #@param login str : provided login
  103. #@param password str : provided password (hash)
  104. #@warning brokes composed UID
  105. #@note implements multiple login/password sources (useless ?)
  106. #@todo composed UID broken method
  107. #@todo allow to provide an authentication source
  108. @classmethod
  109. def authenticate(self, login=None, password=None):
  110. # Authenticate
  111. for infos in self._infos_fields:
  112. logger.debug(self._infos_fields)
  113. login_cls = infos['login'][0]
  114. pass_cls = infos['password'][0]
  115. qfilter = "{passfname} = {passhash}"
  116. uid_fname = login_cls.uid_fieldname()[0] # COMPOSED UID BROKEN
  117. if login_cls == pass_cls:
  118. # Same EmClass for login & pass
  119. qfilter = qfilter.format(
  120. passfname=infos['password'][1],
  121. passhash=password)
  122. else:
  123. # Different EmClass, building a relational filter
  124. passfname = "%s.%s" % (infos['login'][2], infos['password'][1])
  125. qfilter = qfilter.format(
  126. passfname=passfname,
  127. passhash=password)
  128. getq = LeGetQuery(infos['login'][0], qfilter,
  129. field_list=[uid_fname], limit=1)
  130. req = getq.execute()
  131. if len(req) == 1:
  132. self.__set_authenticated(infos['login'][0], req[0][uid_fname])
  133. break
  134. if self.is_anonymous():
  135. self.authentication_failure() # Security logging
  136. ## @brief Attempt to restore a session given a session token
  137. #@param token mixed : a session token
  138. #@return Session data (a dict)
  139. #@throw ClientAuthenticationFailure if token is not valid or not
  140. # existing
  141. @classmethod
  142. def restore_session(cls, token):
  143. cls._assert_instance()
  144. if cls._instance.__session_token is not None:
  145. raise ClientAuthenticationError("Trying to restore a session, but \
  146. a session is already started !!!")
  147. try:
  148. cls._instance.__data = SessionHandler.restore(token)
  149. cls._instance.__session_token = token
  150. except ClientAuthenticationFailure:
  151. logger.warning("Session restoring failed")
  152. return copy.copy(cls._instance.data)
  153. ## @brief Returns the current session token or None
  154. #@return A session token or None
  155. @classmethod
  156. def session_token(cls):
  157. cls._assert_instance()
  158. return cls._instance.__session_token
  159. ## @brief Deletes current session
  160. @classmethod
  161. def destroy(cls):
  162. cls._assert_instance()
  163. SessionHandler.destroy(cls._instance.__session_token)
  164. cls._instance.__session_token = None
  165. cls._instance.__data = dict()
  166. ## @brief Deletes current client and saves its session
  167. @classmethod
  168. def clean(cls):
  169. if cls._instance.__session_token is not None:
  170. SessionHandler.save(cls._instance.__session_token, cls._instance.__data)
  171. if Client._instance is not None:
  172. del(Client._instance)
  173. Client._instance = None
  174. ## @brief Tests if a client is anonymous or logged in
  175. #@return True if client is anonymous
  176. @classmethod
  177. def is_anonymous(cls):
  178. return Client._instance.user() is None
  179. ## @brief Method to be called on authentication failure
  180. #@throw ClientAuthenticationFailure
  181. #@throw LodelFatalError if no Client child instance is found
  182. @classmethod
  183. def authentication_failure(cls):
  184. cls._generic_error(ClientAuthenticationFailure)
  185. ## @brief Method to be called on authentication error
  186. #@throw ClientAuthenticationError
  187. #@throw LodelFatalError if no Client child instance is found
  188. @classmethod
  189. def authentication_error(cls, msg="Unknow error"):
  190. cls._generic_error(ClientAuthenticationError, msg)
  191. ## @brief Method to be called on permission denied error
  192. #@throw ClientPermissionDenied
  193. #@throw LodelFatalError if no Client child instance is found
  194. @classmethod
  195. def permission_denied_error(cls, msg=""):
  196. cls._generic_error(ClientPermissionDenied, msg)
  197. ## @brief Generic error method
  198. #@see Client::authentication_failure() Client::authentication_error()
  199. # Client::permission_denied_error()
  200. #@throw LodelFatalError if no Client child instance is found
  201. @classmethod
  202. def _generic_error(cls, expt, msg=""):
  203. cls._assert_instance()
  204. raise expt(Client._instance, msg)
  205. ## @brief Asserts that an instance of Client child class exists
  206. #@throw LodelFataError if no instance of Client child class is found
  207. @classmethod
  208. def _assert_instance(cls):
  209. if Client._instance is None:
  210. raise LodelFatalError("No client instance found. Abording.")
  211. ## @brief Class method that fetches conf
  212. #
  213. # This method populates Client._infos_fields . This attribute stores
  214. # informations on login and password location (LeApi object & field)
  215. @classmethod
  216. def fetch_settings(cls):
  217. LodelContext.expose_dyncode(globals(), 'dyncode')
  218. if cls._infos_fields is None:
  219. cls._infos_fields = list()
  220. else:
  221. # Already fetched
  222. return
  223. infos = (
  224. Settings.auth.login_classfield,
  225. Settings.auth.pass_classfield)
  226. res_infos = []
  227. for clsname, fieldname in infos:
  228. dcls = dyncode.lowername2class(infos[0][0])
  229. res_infos.append((dcls, infos[1][1]))
  230. link_field = None
  231. if res_infos[0][0] != res_infos[1][0]:
  232. # login and password are in two separated EmClass
  233. # determining the field that links login EmClass to password
  234. # EmClass
  235. for fname, fdh in res_infos[0][0].fields(True).items():
  236. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  237. link_field = fname
  238. if link_field is None:
  239. # Unable to find link between login & password EmClass
  240. raise AuthenticationError("Unable to find a link between \
  241. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  242. res_infos[0][0], res_infos[1][0]))
  243. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  244. cls._infos_fields.append(
  245. {'login': res_infos[0], 'password': res_infos[1]})
  246. ## @brief Sets a user as authenticated and starts a new session
  247. #@param leo LeObject child class : the LeObject the user is stored in
  248. #@param uid str : uniq id (in leo)
  249. #@return None
  250. @classmethod
  251. def __set_authenticated(cls, leo, uid):
  252. cls._instance.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}
  253. # Store auth infos in session
  254. cls._instance.__data[cls._instance.__class__._AUTH_DATANAME] = copy.copy(
  255. cls._instance.__user)