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

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