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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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.datas()[key]
  23. def __delitem__(self, key):
  24. del(self.datas()[key])
  25. def __setitem__(self, key, value):
  26. if self.get_session_token() is None:
  27. self.set_session_token(SessionHandler.start())
  28. datas = self.datas()
  29. datas[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.__datas = dict()
  76. if session_token is not None:
  77. self.__datas = 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.__datas)
  83. @classmethod
  84. def datas(cls):
  85. return cls._instance.__datas
  86. @classmethod
  87. def user(cls):
  88. if '__auth_user_infos' in cls._instance.__datas:
  89. return cls._instance.__datas['__auth_user_infos']
  90. else:
  91. return None
  92. @classmethod
  93. def get_session_token(cls):
  94. return cls._instance.__session_token
  95. @classmethod
  96. def set_session_token(cls, value):
  97. cls._instance.__session_token = value
  98. ##@brief Try to authenticate a user with a login and a password
  99. #@param login str : provided login
  100. #@param password str : provided password (hash)
  101. #@warning brokes composed UID
  102. #@note implemets multiple login/password sources (useless ?)
  103. #@todo composed UID broken method
  104. #@todo allow to provide an authentication source
  105. @classmethod
  106. def authenticate(self, login = None, password = None):
  107. #Authenticate
  108. for infos in self._infos_fields:
  109. logger.debug(self._infos_fields)
  110. login_cls = infos['login'][0]
  111. pass_cls = infos['password'][0]
  112. qfilter = "{passfname} = {passhash}"
  113. uid_fname = login_cls.uid_fieldname()[0] #COMPOSED UID BROKEN
  114. if login_cls == pass_cls:
  115. #Same EmClass for login & pass
  116. qfilter = qfilter.format(
  117. passfname = infos['password'][1],
  118. passhash = password)
  119. else:
  120. #Different EmClass, building a relational filter
  121. passfname = "%s.%s" % (infos['login'][2], infos['password'][1])
  122. qfilter = qfilter.format(
  123. passfname = passfname,
  124. passhash = password)
  125. getq = LeGetQuery(infos['login'][0], qfilter,
  126. field_list = [uid_fname], limit = 1)
  127. req = getq.execute()
  128. if len(req) == 1:
  129. self.__set_authenticated(infos['login'][0],req[0][uid_fname])
  130. break
  131. if self.is_anonymous():
  132. self.authentication_failure() #Security logging
  133. ##@brief Attempt to restore a session given a session token
  134. #@param token mixed : a session token
  135. #@return Session datas (a dict)
  136. #@throw ClientAuthenticationFailure if token is not valid or not
  137. #existing
  138. @classmethod
  139. def restore_session(cls, token):
  140. cls._assert_instance()
  141. if cls._instance.__session_token is not None:
  142. raise ClientAuthenticationError("Trying to restore a session, but \
  143. a session is allready started !!!")
  144. try:
  145. cls._instance.__datas = SessionHandler.restore(token)
  146. cls._instance.__session_token = token
  147. except ClientAuthenticationFailure:
  148. logger.warning("Session restoring fails")
  149. return copy.copy(cls._instance.datas)
  150. ##@brief Return the current session token or None
  151. #@return A session token or None
  152. @classmethod
  153. def session_token(cls):
  154. cls._assert_instance()
  155. return cls._instance.__session_token
  156. ##@brief Delete current session
  157. @classmethod
  158. def destroy(cls):
  159. cls._assert_instance()
  160. SessionHandler.destroy(cls._instance.__session_token)
  161. cls._instance.__session_token = None
  162. cls._instance.__datas = dict()
  163. ##@brief Delete current client and save its session
  164. @classmethod
  165. def clean(cls):
  166. if cls._instance.__session_token is not None:
  167. SessionHandler.save(cls._instance.__session_token, cls._instance.__datas)
  168. if Client._instance is not None:
  169. del(Client._instance)
  170. Client._instance = None
  171. ##@brief Test wether a client is anonymous or logged in
  172. #@return True if client is anonymous
  173. @classmethod
  174. def is_anonymous(cls):
  175. return Client._instance.user() is None
  176. ##@brief Method to call on authentication failure
  177. #@throw ClientAuthenticationFailure
  178. #@throw LodelFatalError if no Client child instance found
  179. @classmethod
  180. def authentication_failure(cls):
  181. cls._generic_error(ClientAuthenticationFailure)
  182. ##@brief Method to call on authentication error
  183. #@throw ClientAuthenticationError
  184. #@throw LodelFatalError if no Client child instance found
  185. @classmethod
  186. def authentication_error(cls, msg = "Unknow error"):
  187. cls._generic_error(ClientAuthenticationError, msg)
  188. ##@brief Method to call on permission denied error
  189. #@throw ClientPermissionDenied
  190. #@throw LodelFatalError if no Client child instance found
  191. @classmethod
  192. def permission_denied_error(cls, msg = ""):
  193. cls._generic_error(ClientPermissionDenied, msg)
  194. ##@brief Generic error method
  195. #@see Client::authentication_failure() Client::authentication_error()
  196. #Client::permission_denied_error()
  197. #@throw LodelFatalError if no Client child instance found
  198. @classmethod
  199. def _generic_error(cls, expt, msg = ""):
  200. cls._assert_instance()
  201. raise expt(Client._instance, msg)
  202. ##@brief Assert that an instance of Client child class exists
  203. #@throw LodelFataError if no instance of Client child class found
  204. @classmethod
  205. def _assert_instance(cls):
  206. if Client._instance is None:
  207. raise LodelFatalError("No client instance found. Abording.")
  208. ##@brief Class method that fetches conf
  209. #
  210. #This method populates Client._infos_fields . This attribute stores
  211. #informations on login and password location (LeApi object & field)
  212. @classmethod
  213. def fetch_settings(cls):
  214. LodelContext.expose_dyncode(globals(), 'dyncode')
  215. if cls._infos_fields is None:
  216. cls._infos_fields = list()
  217. else:
  218. #Allready fetched
  219. return
  220. infos = (
  221. Settings.auth.login_classfield,
  222. Settings.auth.pass_classfield)
  223. res_infos = []
  224. for clsname, fieldname in infos:
  225. dcls = dyncode.lowername2class(infos[0][0])
  226. res_infos.append((dcls, infos[1][1]))
  227. link_field = None
  228. if res_infos[0][0] != res_infos[1][0]:
  229. # login and password are in two separated EmClass
  230. # determining the field that links login EmClass to password
  231. # EmClass
  232. for fname, fdh in res_infos[0][0].fields(True).items():
  233. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  234. link_field = fname
  235. if link_field is None:
  236. #Unable to find link between login & password EmClasses
  237. raise AuthenticationError("Unable to find a link between \
  238. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  239. res_infos[0][0], res_infos[1][0]))
  240. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  241. cls._infos_fields.append(
  242. {'login':res_infos[0], 'password':res_infos[1]})
  243. ##@brief Set a user as authenticated and start a new session
  244. #@param leo LeObject child class : the LeObject the user is stored in
  245. #@param uid str : uniq id (in leo)
  246. #@return None
  247. @classmethod
  248. def __set_authenticated(cls, leo, uid):
  249. cls._instance.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}
  250. #Store auth infos in session
  251. cls._instance.__datas[cls._instance.__class__._AUTH_DATANAME] = copy.copy(cls._instance.__user)