Browse Source

Authentication handlers implementation

- implements a Client abstract singleton class designed to be implemented by UI to be able to register clients informations
- implements a Auth singleton class. Kind of interface between Client singleton and session hanlder plugin
Yann Weber 8 years ago
parent
commit
82f12d95ec
3 changed files with 333 additions and 2 deletions
  1. 27
    1
      lodel/auth/__init__.py
  2. 291
    0
      lodel/auth/auth.py
  3. 15
    1
      lodel/auth/exceptions.py

+ 27
- 1
lodel/auth/__init__.py View File

@@ -1 +1,27 @@
1
-__author__ = 'roland'
1
+##@package lodel.auth Package handling authentication on Lodel2
2
+#
3
+#The authentication mechanism are divided in multiple peaces : 
4
+#- The client ( @ref lodel.auth.auth.Client ) singleton class that stores
5
+#clients infos
6
+#- The @ref lodel.auth.Auth class handles authentication, sessions
7
+#creation/load/deletion
8
+#- The session handler implement as a plugin
9
+#
10
+#@par Client class
11
+#
12
+#The @ref lodel.auth.auth.Client class is an abstract singleton. It is designed
13
+#to be implemented by UI plugins. In fact we don't have the same client
14
+#informations on a web UI, on a CLI or with UDP communications. The main goal
15
+#of this class is to provide an API to interface plugins to stores client
16
+#informations allowing lodel2 to produce security log messages containing 
17
+#client informations.
18
+#
19
+#@par Auth class
20
+#
21
+#The auth class is a singleton designed to actually do authentication.
22
+#This class fetch from settings the Emclass and it's field that contains
23
+#login and password. It's also an API between Client class and session handler
24
+#
25
+#@par Session handler
26
+#
27
+#Implemented as a plugin, called with hooks.

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

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

+ 15
- 1
lodel/auth/exceptions.py View File

@@ -1,12 +1,26 @@
1 1
 from lodel import logger
2 2
 
3
+##@brief Handles common errors on authentication
3 4
 class AuthenticationError(Exception):
4 5
     pass
5 6
 
7
+
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)
15
+        super().__init__(msg)
16
+
17
+
18
+##@brief Handles authentication failure
19
+#
20
+#@note Handle the creation of a security log message containing client info
6 21
 class AuthenticationFailure(Exception):
7 22
     
8 23
     def __init__(self, client):
9 24
         msg = "%s : authentication failure" % client
10 25
         logger.security(msg)
11 26
         super().__init__(msg)
12
-

Loading…
Cancel
Save