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