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