Browse Source

Starting again implementation of Auth/Client/Session global handling

Yann Weber 8 years ago
parent
commit
ed4d2858b9
3 changed files with 265 additions and 326 deletions
  1. 0
    307
      lodel/auth/auth.py
  2. 220
    0
      lodel/auth/client.py
  3. 45
    19
      lodel/auth/exceptions.py

+ 0
- 307
lodel/auth/auth.py View File

@@ -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
-

+ 220
- 0
lodel/auth/client.py View File

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

+ 45
- 19
lodel/auth/exceptions.py View File

@@ -1,26 +1,52 @@
1 1
 from lodel import logger
2 2
 
3
-##@brief Handles common errors on authentication
4
-class AuthenticationError(Exception):
5
-    pass
6
-
3
+##@brief Handles common errors with a Client
4
+class ClientError(Exception):
5
+    ##@brief The logger function to use to log the error message
6
+    _loglvl = logger.warning
7
+    ##@brief Error str
8
+    _err_str = "Error"
9
+    ##@brief the hook name to trigger with error
10
+    _action = 'lodel2_ui_error'
11
+    ##@brief the hook payload
12
+    _payload = None
7 13
 
8
-##@brief Handles authentication error with possible security issue
9
-#
10
-#@note Handle the creation of a security log message containing client info
11
-class AuthenticationSecurityError(AuthenticationError):
12
-    def __init__(self, client):
13
-        msg = "%s : authentication error" % client
14
-        logger.security(msg)
14
+    ##@brief Constructor
15
+    #
16
+    #Log message are build using the following format :
17
+    #"<client infos> : <_err_str>[ : <msg>]"
18
+    def __init__(self, client, msg = ""):
19
+        msg = self.build_message(client, msg)
20
+        if cls._loglvl is not None:
21
+            cls._loglvl(msg)
15 22
         super().__init__(msg)
23
+        if self._action is not None:
24
+            LodelHook.call_hook(self._action, self, self._payload)
25
+
26
+    ##@brief build error message
27
+    def build_message(self, client, msg):
28
+        res = "%s : %s" % (client, self._err_str)
29
+        if len(msg) > 0:
30
+            res += " : %s" % msg
31
+        return res
32
+
33
+##@brief Handles authentication failure errors
34
+class ClientAuthenticationFailure(ClientError):
35
+    _loglvl = logger.security
36
+    _err_str = 'Authentication failure'
37
+    _action = 'lodel2_ui_authentication_failure'
16 38
 
17 39
 
18
-##@brief Handles authentication failure
19
-#
20
-#@note Handle the creation of a security log message containing client info
21
-class AuthenticationFailure(Exception):
40
+##@brief Handles permission denied errors
41
+class ClientPermissionDenied(ClientError):
42
+    _loglvl = logger.security
43
+    _err_str = 'Permission denied'
44
+    _action = 'lodel2_ui_permission_denied'
22 45
     
23
-    def __init__(self, client):
24
-        msg = "%s : authentication failure" % client
25
-        logger.security(msg)
26
-        super().__init__(msg)
46
+
47
+##@brief Handles common errors on authentication
48
+class ClientAuthenticationError(ClientError):
49
+    _loglvl = logger.error
50
+    _err_str = 'Authentication error'
51
+    _action = 'lodel2_ui_error'
52
+

Loading…
Cancel
Save