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.

auth.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. #-*- coding: utf-8 -*-
  2. from lodel.settings import Settings
  3. from lodel import logger
  4. from lodel.plugin.hooks import LodelHook
  5. from lodel.leapi.query import LeGetQuery
  6. from lodel.exceptions import *
  7. from .exceptions import *
  8. ##@brief Abstract class designed to be implemented by plugin interfaces
  9. #
  10. #A singleton class storing current client informations.
  11. #
  12. #For the moment the main goal is to be able to produce security log
  13. #containing well formated client informations
  14. class Client(object):
  15. ##@brief Stores the singleton instance
  16. _instance = None
  17. ##@brief Implements singleton behavior
  18. def __init__(self):
  19. if self.__class__ == Client:
  20. raise NotImplementedError("Abstract class")
  21. logger.debug("New instance of %s" % self.__class__.__name__)
  22. if self._instance is not None:
  23. old = self._instance
  24. self._instance = None
  25. del(old)
  26. logger.debug("Replacing old Client instance by a new one")
  27. Client._instance = self
  28. logger.debug("New client : %s" % self)
  29. # Instanciation done. Triggering Auth instanciation
  30. self.__auth = Auth(self)
  31. ##@brief Destructor
  32. #@note calls Auth destructor too
  33. def __del__(self):
  34. del(self.__auth)
  35. ##@brief Abstract method.
  36. #
  37. #@note Used to generate security log message. Avoid \n etc.
  38. def __str__(self):
  39. raise NotImplementedError("Abstract method")
  40. #
  41. # Utility methods (Wrapper for Auth)
  42. #
  43. ##@brief Return current instance or raise an Exception
  44. #@throw AuthenticationError
  45. @classmethod
  46. def client(cls):
  47. if cls._instance is None:
  48. raise LodelFatalError("Calling a Client classmethod but no Client \
  49. instance exists")
  50. return cls._instance
  51. ##@brief Alias of Client::destroy()
  52. @classmethod
  53. def deauth(cls):
  54. cls.destroy()
  55. ##@brief Destroy current client
  56. @classmethod
  57. def destroy(cls):
  58. inst = cls._instance
  59. cls._instance = None
  60. del(inst)
  61. ##@brief Authenticate using login an password
  62. #@note Wrapper on Auth.auth()
  63. #@param login str
  64. #@param password str
  65. #@return None
  66. #@throw AuthenticationFailure
  67. #@see Auth.auth()
  68. @classmethod
  69. def auth_password(cls, login, password):
  70. cls.client.auth(login, password)
  71. ##@brief Authenticate using session token
  72. #@param token str : session token
  73. #@throw AuthenticationFailure
  74. #@see Auth.auth_session()
  75. @classmethod
  76. def auth_session(cls, token):
  77. cls.client.auth_session(token)
  78. ##@brief Generic authentication method
  79. #
  80. #Possible arguments are :
  81. # - authenticate(token) ( see @ref Client.auth_session() )
  82. # - authenticate(login, password) ( see @ref Client.auth_password() )
  83. #@param *args
  84. #@param **kwargs
  85. @classmethod
  86. def authenticate(cls, *args, **kwargs):
  87. token = None
  88. login_pass = None
  89. if 'token' in kwargs:
  90. #auth session
  91. if len(args) != 0 or len(kwargs) != 0:
  92. # security issue ?
  93. raise AuthenticationSecurityError(cls.client())
  94. else:
  95. session = kwargs['token']
  96. elif len(args) == 1:
  97. if len(kwargs) == 0:
  98. #Auth session
  99. token = args[0]
  100. elif len(kwargs) == 1:
  101. if 'login' in kwargs:
  102. login_pass = (kwargs['login'], args[0])
  103. elif 'password' in kwargs:
  104. login_pass = (args[0], kwargs['password'])
  105. elif len(args) == 2:
  106. login_pass = tuple(args)
  107. if login_pass is None and token is None:
  108. # bad arguments given. Security issue ?
  109. raise AuthenticationSecurityError(cls.client())
  110. elif login_pass is None:
  111. cls.auth_session(token)
  112. else:
  113. cls.auth_password(*login_pass)
  114. ##@brief Singleton class that handles authentication on lodel2 instances
  115. #
  116. #
  117. #@note Designed to be never called directly. The Client class is designed to
  118. #be implemented by UI and to provide a friendly/secure API for \
  119. #client/auth/session handling
  120. #@todo specs of client infos given as argument on authentication methods
  121. class Auth(object):
  122. ##@brief Stores singleton instance
  123. _instance = None
  124. ##@brief List of dict that stores field ref for login and password
  125. #
  126. # Storage specs :
  127. #
  128. # A list of dict, with keys 'login' and 'password', items are tuple.
  129. #- login tuple contains (LeObjectChild, FieldName, link_field) with:
  130. # - LeObjectChild the dynclass containing the login
  131. # - Fieldname the fieldname of LeObjectChild containing the login
  132. # - link_field None if both login and password are in the same
  133. # LeObjectChild. Else contains the field that make the link between
  134. # login LeObject and password LeObject
  135. #- password typle contains (LeObjectChild, FieldName)
  136. _infos_fields = None
  137. ##@brief Constructor
  138. #
  139. #@note Automatic clean of previous instance
  140. def __init__(self, client):
  141. ##@brief Stores infos about logged in user
  142. #
  143. #Tuple containing (LeObjectChild, UID) of logged in user
  144. self.__user_infos = False
  145. ##@brief Stores session id
  146. self.__session_id = False
  147. if not isinstance(client, Client):
  148. msg = "<class Client> instance was expected but got %s"
  149. msg %= type(client)
  150. raise TypeError(msg)
  151. ##@brief Stores client infos
  152. self.__client = client
  153. # Singleton
  154. if self._instance is not None:
  155. bck = self._instance
  156. bck.destroy()
  157. self._instance = None
  158. logger.debug("Previous Auth instance replaced by a new one")
  159. else:
  160. #First instance, fetching settings
  161. self.fetch_settings()
  162. self.__class__._instance = self
  163. ##@brief Destroy current instance an associated session
  164. def _destroy(self):
  165. self.__user_infos = LodelHook.call_hook('lodel2_session_destroy',
  166. caller = self, payload = self.__session_id)
  167. ##@brief Destroy singleton instance
  168. @classmethod
  169. def destroy(cls):
  170. cls._instance._destroy()
  171. ##@brief Raise exception because of authentication failure
  172. #@note trigger a security log containing client infos
  173. #@throw LodelFatalError if no instance exsists
  174. #@see Auth.fail()
  175. @classmethod
  176. def failure(cls):
  177. if cls._instance is None:
  178. raise LodelFatalError("No Auth instance found. Abording")
  179. raise AuthenticationFailure(cls._instance.fail())
  180. ##@brief Class method that fetches conf
  181. @classmethod
  182. def fetch_settings(cls):
  183. from lodel import dyncode
  184. if cls._infos_fields is None:
  185. cls._infos_fields = list()
  186. else:
  187. #Allready fetched
  188. return
  189. infos = (
  190. Settings.auth.login_classfield,
  191. Settings.auth.pass_classfield)
  192. res_infos = []
  193. for clsname, fieldname in infos:
  194. dcls = dyncode.lowername2class(infos[0][0])
  195. res_infos.append((dcls, infos[1][1]))
  196. link_field = None
  197. if res_infos[0][0] != res_infos[1][0]:
  198. # login and password are in two separated EmClass
  199. # determining the field that links login EmClass to password
  200. # EmClass
  201. for fname, fdh in res_infos[0][0].fields(True).items():
  202. if fdh.is_reference() and res_infos[1][0] in fdh.linked_classes():
  203. link_field = fname
  204. if link_field is None:
  205. #Unable to find link between login & password EmClasses
  206. raise AuthenticationError("Unable to find a link between \
  207. login EmClass '%s' and password EmClass '%s'. Abording..." % (
  208. res_infos[0][0], res_infos[1][0]))
  209. res_infos[0] = (res_infos[0][0], res_infos[0][1], link_field)
  210. cls._infos_fields.append(
  211. {'login':res_infos[0], 'password':res_infos[1]})
  212. ##@brief Raise an AuthenticationFailure exception
  213. #
  214. #@note Trigger a security log message containing client infos
  215. def fail(self):
  216. raise AuthenticationFailure(self.__client)
  217. ##@brief Is the user anonymous ?
  218. #@return True if no one is logged in
  219. def is_anon(self):
  220. return self._login is False
  221. ##@brief Authenticate using a login and a password
  222. #@param login str : provided login
  223. #@param password str : provided password
  224. #@todo automatic hashing
  225. #@warning brokes multiple UID
  226. #@note implements multiple login/password sources (useless ?)
  227. #@todo composed UID broken in this method
  228. def auth(self, login = None, password = None):
  229. # Authenticate
  230. for infos in self._infos_fields:
  231. login_cls = infos['login'][0]
  232. pass_cls = infos['pass'][0]
  233. qfilter = "passfname = passhash"
  234. uid_fname = login_cls.uid_fieldname()[0] #COMPOSED UID BROKEN
  235. if login_cls == pass_cls:
  236. #Same EmClass for login & pass
  237. qfilter = qfilter.format(
  238. passfname = infos['pass'][1],
  239. passhash = password)
  240. else:
  241. #Different EmClass, building a relational filter
  242. passfname = "%s.%s" % (infos['login'][2], infos['pass'][1])
  243. qfilter = qfilter.format(
  244. passfname = passfname,
  245. passhash = password)
  246. getq = LeGetQuery(infos['login'][0], qfilter,
  247. field_list = [uid_fname], limit = 1)
  248. req = getq.execute()
  249. if len(req) == 1:
  250. #Authenticated
  251. self.__set_authenticated(infos['login'][0], req[uid_fname])
  252. break
  253. if self.is_anon():
  254. self.fail() #Security logging
  255. ##@brief Authenticate using a session token
  256. #@note Call a dedicated hook in order to allow session implementation as
  257. #plugin
  258. #@thrown AuthenticationFailure
  259. def auth_session(self, token):
  260. try:
  261. self.__user_infos = LodelHook.call_hook('lodel2_session_load',
  262. caller = self, payload = token)
  263. except AuthenticationError:
  264. self.fail() #Security logging
  265. self.__session_id = token
  266. ##@brief Set a user as authenticated and start a new session
  267. #@param leo LeObject child class : The EmClass the user belong to
  268. #@param uid str : uniq ID (in leo)
  269. #@return None
  270. def __set_authenticated(self, leo, uid):
  271. # Storing user infos
  272. self.__user_infos = {'classname': leo.__name__, 'uid': uid}
  273. # Init session
  274. sid = LodelHook.call_hook('lodel2_session_start', caller = self,
  275. payload = copy.copy(self.__user_infos))
  276. self.__session_id = sid