Ei kuvausta
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

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