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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #-*- Coding: utf-8 -*-
  2. import copy
  3. from lodel.settings import Settings
  4. from lodel import logger
  5. from lodel.plugin.hooks import LodelHook
  6. from lodel.plugin import SessionHandlerPlugin as SessionHandler
  7. ##@brief Client metaclass designed to implements container accessor on
  8. #Client Class
  9. class ClientMetaclass(type):
  10. SESSION_ID_NAME = '__SESSION_ID__'
  11. def __init__(self, name, bases, attrs):
  12. self.__session = dict()
  13. return super(ClientMetaclass, self).__init__(name, bases, attrs)
  14. def __getitem__(self, key):
  15. if key not in self.__session:
  16. raise KeyError("This client instance does not have a '%s' data" % key)
  17. return self.__session[key]
  18. def __setitem__(self, key, value):
  19. if SESSION_ID_NAME not in self.__session:
  20. self.__session[SESSION_ID_NAME] = SessionHandler.start_session()
  21. self.__session[key] = value
  22. ##@brief Return a copy of sessions infos
  23. def session_dump(self):
  24. return copy.copy(self.__session)
  25. ##@brief Abstract singleton class designed to handle client informations
  26. #
  27. # This class is designed to handle client authentication and sessions
  28. class Client(object, metaclass = ClientMetaclass):
  29. ##@brief Singleton instance
  30. _instance = None
  31. ##@brief List of dict that stores field ref for login and password
  32. #
  33. # Storage specs :
  34. #
  35. # A list of dict, with keys 'login' and 'password', items are tuple.
  36. #- login tuple contains (LeObjectChild, FieldName, link_field) with:
  37. # - LeObjectChild the dynclass containing the login
  38. # - Fieldname the fieldname of LeObjectChild containing the login
  39. # - link_field None if both login and password are in the same
  40. # LeObjectChild. Else contains the field that make the link between
  41. # login LeObject and password LeObject
  42. #- password typle contains (LeObjectChild, FieldName)
  43. _infos_fields = None
  44. ##@brief Constructor
  45. #@param ui_instance Lodel2Ui child class instance
  46. #@param client_infos mixed : Depends on UI implemetation
  47. #@param session_token mixed : Session token provided by client to interface
  48. def __init__(self,ui_instance, client_infos, session_token = None):
  49. if self.__class__ == Client:
  50. raise NotImplementedError("Abstract class")
  51. logger.debug("New instance of Client child class %s" %
  52. self.__class__.__name__)
  53. if Client._instance is not None:
  54. old = Client._instance
  55. Client._instance = None
  56. del(old)
  57. logger.debug("Replacing old Client instance by a new one")
  58. else:
  59. #first instanciation, fetching settings
  60. self.fetch_settings()
  61. ##@brief Stores instance of UI
  62. self.__ui_instance = ui_instance
  63. ##@brief Stores infos for authenticated users (None == anonymous)
  64. self.__user = None
  65. ##@brief Stores the session handler
  66. Client._instance = self
  67. logger.debug("New client : %s" % self)
  68. ##@brief Attempt to restore a session given a session token
  69. #@param token mixed : a session token
  70. #@return Session datas (a dict)
  71. #@throw ClientAuthenticationFailure if token is not valid or not
  72. #existing
  73. def _restore_session(self, token):
  74. res = self._session_handler.restore_session(token)
  75. if res is False:
  76. raise ClientAuthenticationFailure(client = self,
  77. msg = "Invalid or not existing session token provided")
  78. pass
  79. ##@brief Try to authenticate a user with a login and a password
  80. #@param login str : provided login
  81. #@param password str : provided password (hash)
  82. #@warning brokes composed UID
  83. #@note implemets multiple login/password sources (useless ?)
  84. #@todo composed UID broken method
  85. #@todo allow to provide an authentication source
  86. @classmethod
  87. def authenticate(self, login = None, password = None):
  88. #Authenticate
  89. for infos in self._infos_fields:
  90. login_cls = infos['login'][0]
  91. pass_cls = infos['pass'][0]
  92. qfilter = "{passfname} = {passhash}"
  93. uid_fname = login_cls.uid_fieldname()[0] #COMPOSED UID BROKEN
  94. if login_cls == pass_cls:
  95. #Same EmClass for login & pass
  96. qfilter = qfilter.format(
  97. passfname = infos['pass'][1],
  98. passhash = password)
  99. else:
  100. #Different EmClass, building a relational filter
  101. passfname = "%s.%s" % (infos['login'][2], infos['pass'][1])
  102. qfilter = qfilter.format(
  103. passfname = passfname,
  104. passhash = password)
  105. getq = LeGetQuery(infos['login'][0], qfilter,
  106. field_list = [uid_fname], limit = 1)
  107. req = getq.execute()
  108. if len(req) == 1:
  109. #Authenticated
  110. self.__set_authenticated(infos['login'][0], req[uid_fname])
  111. break
  112. if self.is_anon():
  113. self.fail() #Security logging
  114. ##@brief Test wether a client is anonymous or logged in
  115. #@return True if client is anonymous
  116. @classmethod
  117. def is_anonymous(cls):
  118. cls._assert_instance()
  119. return Client._instance
  120. ##@brief Method to call on authentication failure
  121. #@throw ClientAuthenticationFailure
  122. #@throw LodelFatalError if no Client child instance found
  123. @classmethod
  124. def authentication_failure(cls):
  125. cls._generic_error(ClientAuthenticationFailure)
  126. ##@brief Method to call on authentication error
  127. #@throw ClientAuthenticationError
  128. #@throw LodelFatalError if no Client child instance found
  129. @classmethod
  130. def authentication_error(cls, msg = "Unknow error"):
  131. cls._generic_error(ClientAuthenticationError, msg)
  132. ##@brief Method to call on permission denied error
  133. #@throw ClientPermissionDenied
  134. #@throw LodelFatalError if no Client child instance found
  135. @classmethod
  136. def permission_denied_error(cls, msg = ""):
  137. cls._generic_error(ClientPermissionDenied, msg)
  138. ##@brief Generic error method
  139. #@see Client::authentication_failure() Client::authentication_error()
  140. #Client::permission_denied_error()
  141. #@throw LodelFatalError if no Client child instance found
  142. @classmethod
  143. def _generic_error(cls, expt, msg = ""):
  144. cls._assert_instance()
  145. raise expt(Client._instance, msg)
  146. ##@brief Assert that an instance of Client child class exists
  147. #@throw LodelFataError if no instance of Client child class found
  148. @classmethod
  149. def _assert_instance(cls):
  150. if Client._instance is None:
  151. raise LodelFatalError("No client instance found. Abording.")
  152. ##@brief Class method that fetches conf
  153. #
  154. #This method populates Client._infos_fields . This attribute stores
  155. #informations on login and password location (LeApi object & field)
  156. @classmethod
  157. def fetch_settings(cls):
  158. from lodel import dyncode
  159. if cls._infos_fields is None:
  160. cls._infos_fields = list()
  161. else:
  162. #Allready fetched
  163. return
  164. infos = (
  165. Settings.auth.login_classfield,
  166. Settings.auth.pass_classfield)
  167. res_infos = []
  168. for clsname, fieldname in infos:
  169. dcls = dyncode.lowername2class(infos[0][0])
  170. res_infos.append((dcls, infos[1][1]))
  171. link_field = None
  172. if res_infos[0][0] != res_infos[1][0]:
  173. # login and password are in two separated EmClass
  174. # determining the field that links login EmClass to password
  175. # EmClass
  176. for fname, fdh in res_infos[0][0].fields(True).items():
  177. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  178. link_field = fname
  179. if link_field is None:
  180. #Unable to find link between login & password EmClasses
  181. raise AuthenticationError("Unable to find a link between \
  182. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  183. res_infos[0][0], res_infos[1][0]))
  184. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  185. cls._infos_fields.append(
  186. {'login':res_infos[0], 'password':res_infos[1]})
  187. ##@brief Set a user as authenticated and start a new session
  188. #@param leo LeObject child class : the LeObject the user is stored in
  189. #@param uid str : uniq id (in leo)
  190. #@return None
  191. def __set_authenticated(self, leo, uid):
  192. self.__user = {'classname': leo.__name__, 'uid': uid, 'leoclass': leo}