*** empty log message ***
[pli.git] / pli / net / rpcsession.py
blob95d51646017279cfe70f646e20674b722c980200
1 #=======================================================================
3 __version__ = '''0.1.42'''
4 __sub_version__ = '''20041019040755'''
5 __copyright__ = '''(c) Alex A. Naanou 2003'''
8 #=======================================================================
10 import time
11 import random
12 import sha
14 import pli.interface as interface
15 ##!! remove ...
16 import pli.misc.acl as acl
17 import pli.misc.passcrypt as passcrypt
20 #-----------------------------------------------------------------------
21 #--------------------------------------------------------SessionError---
22 class SessionError(Exception):
23 '''
24 '''
28 #-----------------------------------------------------------------------
29 #-------------------------------------------------------------Session---
30 # use this for ACL and basic session event handlers...
31 # TODO revise security settings!!!
32 # TODO revise mix-in criteria...
33 class Session(object):
34 '''
35 this is the generic RPC session class.
37 this provides basic password support.
38 '''
39 # this is True if this object is public
40 # NOTE: for the session object this MUST be True
41 # NOTE: this will override the settings of the containers'
42 # private/public attrs...
43 __public__ = True
45 # setup interface...
46 interface.inherit(iname='ISession')
47 # private...
48 interface.private('__public__')
49 interface.private('__private_attrs__')
50 interface.private('__public_attrs__')
51 interface.private('__selector_class__')
52 interface.private('_onSessionOpen')
53 interface.private('_onSessionClose')
54 interface.private('onSessionOpen')
55 interface.private('onSessionClose')
56 interface.private('checkpassword')
57 interface.private('_setpassword')
58 interface.private('password')
59 # public...
60 interface.add('changepassword', writable=False, deleteable=False)
62 # this will define the private attributes
63 __private_attrs__ = (
64 '__public__',
65 '__private_attrs__',
66 '__public_attrs__',
67 '__selector_class__',
68 '_onSessionOpen',
69 '_onSessionClose',
70 'onSessionOpen',
71 'onSessionClose',
72 'checkpassword',
73 '_setpassword',
74 'password',
77 def __getattr__(self, name):
78 '''
79 '''
80 if hasattr(self, '__session_dict__') and name in self.__session_dict__:
81 return self.__session_dict__[name]
82 raise AttributeError, '%s object has no attribute "%s"' % (self, name)
83 def __getstate__(self):
84 '''
85 this is a generic getstate...
86 '''
87 d = self.__dict__.copy()
88 if '__session_dict__' in d:
89 del d['__session_dict__']
90 return d
91 def __setstate__(self, data):
92 '''
93 this is a generic setstate mothod...
94 '''
95 self.__dict__.update(data)
96 # LL password and security...
97 def _setpassword(self, password):
98 '''
99 '''
100 self.password = passcrypt.passcrypt_md5(password)
101 def checkpassword(self, password):
104 if hasattr(self, 'password') and self.password not in ('', None):
105 return passcrypt.check_password_md5(password, self.password)
106 return True
107 # User level password and security...
108 def changepassword(self, old_pswd, new_pswd):
111 # check the old password...
112 if not self.checkpassword(old_pswd):
113 raise SessionError, 'can\'t change password.'
114 # change the password...
115 self._setpassword(new_pswd)
116 # low level event handlers...
117 def _onSessionOpen(self, manager):
120 # set the temporary namespace...
121 ## if not hasattr(self, '__session_dict__'):
122 self.__session_dict__ = {}
123 # fire the event...
124 if hasattr(self, 'onSessionOpen'):
125 self.onSessionOpen(manager)
126 def _onSessionClose(self, manager):
129 # delete the temporary namespace...
130 if hasattr(self, '__session_dict__'):
131 del self.__session_dict__
132 # fire the event...
133 if hasattr(self, 'onSessionClose'):
134 self.onSessionClose(manager)
135 # Session event handlers
136 # NOTE: these are here for documentation only...
137 ## def onSessionOpen(self, manager):
138 ## '''
139 ## this is called on session open.
141 ## a session manager object is passed in...
142 ## '''
143 ## def onSessionClose(self, manager):
144 ## '''
145 ## this is called on session open.
147 ## a session manager object is passed in...
148 ## '''
152 #-----------------------------------------------------------------------
153 #---------------------------------------------------RPCSessionManager---
154 # TODO split this into diferent functionality primitives...
155 # - *global method call* functionality.
156 # - session
157 # - ...
158 # TODO global method install/unistall (may be session speciffic...)
159 # TODO define a help protocol:
160 # __doc__
161 # __attrhelp__
162 # __signaturehelp__
163 # ...
165 class RPCSessionManager(object):
167 this class provides the generic RPC session interface.
169 # this will define the active session container object (defaults to dict).
170 __active_sessions__ = None
171 # this will define the session object container (dict-like).
172 __session_objects__ = None
173 # this will define the proxy object that will be used to wrap the
174 # session object...
175 # this will take the session object as argument and return the
176 # proxy.
177 __session_proxy__ = None
178 # if this is set password checking will be enabled...
179 # NOTE: for this to function the session object must have a
180 # "checkpassword" method (see the Session class for details).
181 __password_check__ = False
182 # define the acl lib...
183 __acl_lib__ = acl
184 # enable acl for object access (this should be handled by the
185 # object its self)....
186 __path_acl_check__ = True
187 # if set for an object the path form that object and down will not
188 # be acl checked by the interface...
189 ##!!! test !!##
190 __acl_check_cutoff__ = False
191 # charactes allowed in a SID...
192 # NOTE: canging this in runtime will have no effect...
193 __sid_chars__ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
194 'abcdefghijklmnopqrstuvwxyz' \
195 '0123456789' \
196 './_-&%@#'
197 # this sets the legth of the sid...
198 __sid_len__ = 27
199 # Global method data:
200 # NOTE: there are two interfaces currently supported, one of which
201 # might get depricated...
202 # interface_I:
203 # this defines the list of accesible global methods...
204 __global_methods__ = (
205 ## '_test_I',
207 # this defines the list of non=system methods available without a
208 # session...
209 __public_methods__ = (
211 # this defines system (special-case) methods...
212 __system_methods__ = (
213 'new_session',
214 'close_session',
215 'isalive',
217 # interface_II:
218 # global method naming convention:
219 # <__globalmethodprefix__> % <public_method_name>
220 __globalmethodnameformat__ = 'GLOBAL_%s'
222 # this sets the session global timeout (idle time before session
223 # close), if None timeout checks will be disabled.
224 __timeout__ = 1800.0
226 def __init__(self):
229 if self.__active_sessions__ == None:
230 self.__active_sessions__ = {}
231 if hasattr(self, '__sid_len__') and self.__sid_len__ < 8:
232 print >> sys.stderr, "WARNING: it is recommended that the SID be longer than 8 chars."
233 # System methods (external):
234 def new_session(self, obj_id, password='', *p, **n):
236 this will create a new session.
238 sid_len = hasattr(self, '__sid_len__') and self.__sid_len__ or 32
239 # make sure the sid is unique... (just in case... be pedantic! :) )
240 while 1:
241 # generate a unique sid...
242 sid = self._new_sid(length=sid_len)
243 if sid not in self.__active_sessions__:
244 # select a session object...
245 obj = self.__session_objects__.get(obj_id, None)
246 ## if obj_id not in self.__session_objects__:
247 if obj == None:
248 raise SessionError, 'no such object ("%s").' % obj_id
249 ## obj = self.__session_objects__[obj_id]
250 # check password...
251 if hasattr(self, '__password_check__') and self.__password_check__ and \
252 hasattr(obj, 'checkpassword') and not obj.checkpassword(password):
253 raise SessionError, 'no such object ("%s").' % obj_id
254 # check uniqueness...
255 if hasattr(obj, '__unique_session__') and obj.__unique_session__ and obj in self.__active_sessions__.values():
256 raise SessionError, 'can\'t open two sessions for this object (%s).' % obj_id
257 # proxy...
258 if hasattr(self, '__session_proxy__') and self.__session_proxy__ != None:
259 obj = self.__session_proxy__(obj)
260 # add to list of active sessions...
261 self.__active_sessions__[sid] = obj
262 # set the time...
263 if self.__timeout__ != None:
264 obj._last_accessed = time.time()
265 # fire _onSessionOpen event
266 if hasattr(obj, '_onSessionOpen'):
267 obj._onSessionOpen(self)
268 break
269 return sid
270 def close_session(self, sid, *p, **n):
272 this will close an existing session.
274 if sid not in self.__active_sessions__:
275 return
276 # fire _onSessionClose event
277 obj = self.__active_sessions__[sid]
278 if hasattr(obj, '_onSessionClose'):
279 obj._onSessionClose(self)
280 del self.__active_sessions__[sid]
281 def isalive(self, sid):
283 this will test if a session exists.
285 NOTE: this does not extend the life of the session object.
287 if sid not in self.__active_sessions__ or \
288 self.__timeout__ != None and \
289 (time.time() - self.__active_sessions__[sid]._last_accessed) > self.__timeout__:
290 self.close_session(sid)
291 return False
292 return True
293 # System methods (internal):
294 def dispatch(self, method, *pargs, **nargs):
297 # system methods...
298 if method in self.__system_methods__ + self.__public_methods__:
299 return getattr(self, method)(*pargs, **nargs)
300 # dispatch...
301 else:
302 # dispatch the call to a valid session object...
303 if len(pargs) < 1:
304 raise TypeError, 'argument list too short (must be 1 or more arguments; got %d).' % len(pargs)
305 sid = str(pargs[0])
306 # check session object age...
307 if not self.isalive(sid):
308 raise SessionError, 'no such session.'
309 path = method.split('.')
310 # get the session...
311 session_obj = self.__active_sessions__[sid]
312 # set access time...
313 if self.__timeout__ != None:
314 session_obj._last_accessed = time.time()
315 # check if we are using a global method...
316 if path[-1] in self.__global_methods__:
317 # call the global method...
318 return self._callglobal(sid, path[:-1], path[-1], *pargs[1:], **nargs)
319 if hasattr(self, self.__globalmethodnameformat__ % path[-1]):
320 # call the global method...
321 return self._callglobal(sid, path[:-1], self.__globalmethodnameformat__ % path[-1], *pargs[1:], **nargs)
322 # dispatch the call to the actual object...
323 return self._getobject(sid, session_obj, path)(*pargs[1:], **nargs)
324 # Utility Methods:
325 def _callglobal(self, sid, path, meth, *p, **n):
327 this will call the global method and handel security...
329 ## if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
330 ## acl = self.__acl_lib__
331 ## else:
332 ## global acl
333 obj = self._getobject(sid, self.__active_sessions__[sid], path)
334 # check acl...
335 if acl.isglobalmethodallowed(obj, meth):
336 # call the method
337 return getattr(self, meth)(sid, path, obj, *p, **n)
338 ##!!! recheck security !!!##
339 def _getobject(self, sid, obj, path):
341 get an object by its path (with ACL).
343 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
344 acl = self.__acl_lib__
345 ## else:
346 ## global acl
347 if hasattr(self, '__path_acl_check__') and self.__path_acl_check__:
348 acl_check_cutoff = False
349 for obj_name in path:
350 if not acl_check_cutoff:
351 # check if this attr is accessible...
352 obj = acl.getattr(obj, obj_name)
353 if hasattr(obj, '__acl_check_cutoff__') and obj.__acl_check_cutoff__:
354 acl_check_cutoff = True
355 else:
356 obj = getattr(obj, obj_name)
357 else:
358 for obj_name in path:
359 obj = getattr(obj, obj_name)
360 return obj
361 def _new_sid(self, length=20, seed=1):
363 will generate a unique session id...
365 if seed == 0 or type(seed) not in (int, long, float):
366 raise TypeError, 'the seed can not be: %s' % seed
367 # define some data...
368 res = ''
369 # cache the vars...
370 c = self.__sid_chars__
371 lc = len(c) - 1
372 rr = random.randrange
373 # NOTE: this is quite brain-dead so will likely be rewritten...
374 while len(res) < length:
375 # generate the translation table...
376 translation_table = ''.join([c[rr(0,lc)] for i in range(256)])
377 # generate the key...
378 res += str(sha.new(str(time.time()*seed)).digest()).translate(translation_table)
379 # truncate the result to the desired length....
380 return res[:length]
381 def check_sessions(self):
384 for session in self.__active_sessions__.keys():
385 if not self.isalive(session):
386 name = self.__active_sessions__[session].__name__
387 ##!! rewrite (with logger...) !!##
388 print '[%s] session for "%s" terminated on timeout.' % (time.strftime('%Y%m%d%H%M%S'), name)
389 # Global Methods:
390 # these are generic clobal methods.
391 # all global methods must be of the form:
392 # <method>(sid, path, obj, ...) -> res
393 # where:
394 # sid : session id
395 # path : list of path nodes
396 # obj : the actual target object.
397 # NOTE: these are not intended for direct use...
398 # the following two are for testing and illustration of the
399 # interface usage...
400 ## def _test_I(self, sid, path, obj, *p, **n):
401 ## '''
402 ## this is here for testing only (interface I)...
403 ## '''
404 ## return str({'sid':sid, 'path':path, 'obj':obj, 'p':p, 'n':n})
405 ## def GLOBAL__test_II(self, sid, path, obj, *p, **n):
406 ## '''
407 ## this is here for testing only (interface II)...
408 ## '''
409 ## return str({'sid':sid, 'path':path, 'obj':obj, 'p':p, 'n':n})
412 #-----------------------------------------------BaseRPCSessionManager---
413 class BaseRPCSessionManager(RPCSessionManager):
415 this class defines the basic object interface for RPCSessionManager.
417 __active_sessions__ = None
418 __session_objects__ = None
420 # Global method data:
421 # this defines the list of accesible global methods...
422 __global_methods__ = RPCSessionManager.__global_methods__ + \
424 'hasattr',
425 'getattr',
426 'getattrs',
427 'setattr',
428 'setattrs',
429 'get_methods',
430 'get_doc',
432 # this defines the list of non-system methods available without a
433 # session...
434 __public_methods__ = RPCSessionManager.__public_methods__ + \
436 'listMethods',
437 ## 'methodSignature',
438 'methodHelp',
439 'new_session'
442 # TODO make acl optional for these.... (ACL is the objects
443 # responsibility (not the interface...))
444 # Global Methods:
445 def hasattr(self, sid, path, obj, name):
448 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
449 acl = self.__acl_lib__
450 return acl.hasattr(obj, name)
451 else:
452 return hasattr(obj, name)
453 def getattr(self, sid, path, obj, name):
456 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
457 acl = self.__acl_lib__
458 # form the result
459 return acl.getattr(obj, name)
460 else:
461 # form the result
462 return getattr(obj, name)
463 def getattrs(self, sid, path, obj, *names):
466 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
467 acl = self.__acl_lib__
468 _getattr = acl.getattr
469 else:
470 _getattr = getattr
471 ## # get the actual target...
472 ## obj = self._getobject(sid, self.__active_sessions__[sid], path)
473 # form the result
474 res = {}
475 for name in names:
476 # this will return only the allowed attributes...
477 try:
478 res[name] = _getattr(obj, name)
479 except:
480 pass
481 return res
482 def setattr(self, sid, path, obj, name, val):
485 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
486 acl = self.__acl_lib__
487 return acl.setattr(obj, name, val)
488 else:
489 return setattr(obj, name, val)
490 def setattrs(self, sid, path, obj, data):
493 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
494 acl = self.__acl_lib__
495 _setattr = acl.setattr
496 else:
497 _setattr = setattr
498 err = False
499 for key in data:
500 try:
501 _setattr(obj, key, data[key])
502 except:
503 err = True
504 if err:
505 return False
506 def delattr(self, sid, path):
509 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
510 acl = self.__acl_lib__
511 # form the result
512 return acl.delattr(obj, name)
513 else:
514 return delattr(obj, name)
515 ##!!! remove dep on __public_attrs__... (???)
516 def get_methods(self, sid, path, obj):
519 if hasattr(self, '__acl_lib__') and self.__acl_lib__ != None:
520 acl = self.__acl_lib__
521 ## else:
522 ## global acl
523 # call the doc makic method if it exists...
524 if hasattr(obj, '__method_doc__'):
525 ##!!!
526 return obj.__method_doc__()
527 res = {}
528 # session/object specific methods...
529 if hasattr(obj, '__public_attrs__'):
530 lst = list(obj.__public_attrs__)
531 else:
532 lst = dir(obj)
533 for attr in lst:
534 try:
535 o = acl.getattr(obj, attr)
536 except AttributeError:
537 continue
538 else:
539 if callable(o) and acl.isaccessible(o):
540 if hasattr(o, '__doc__') and type(o.__doc__) is str:
541 res[attr] = o.__doc__
542 else:
543 res[attr] = ''
544 # do global methods (interface I)...
545 if hasattr(self, '__global_methods__') or True:
546 for meth in self.__global_methods__:
547 o = getattr(self, meth)
548 if hasattr(o, '__doc__') and type(o.__doc__) is str:
549 res[meth] = o.__doc__
550 else:
551 res[meth] = ''
552 # do global methods (interface II)...
553 lst = dir(self)
554 pattern = self.__globalmethodnameformat__.split('%s')
555 for attr in lst:
556 if attr.startswith(pattern[0]) and attr.endswith(pattern[-1]):
557 o = getattr(self, attr)
558 meth = '' not in pattern and attr.split(pattern[0])[-1].split(pattern[-1])[0] or \
559 pattern[0] != '' and attr.split(pattern[0])[-1] or \
560 pattern[-1] != '' and attr.split(pattern[-1])[0]
561 # sanity check...
562 if meth == '':
563 continue
564 if hasattr(o, '__doc__') and type(o.__doc__) is str:
565 res[meth] = o.__doc__
566 else:
567 res[meth] = ''
568 return res
569 # NOTE: this may change in the future...
570 # XXX this appears not to work only for StoreClient.type....
571 def get_doc(self, sid, path, obj):
573 this will print the objects __doc__ and the return
574 of the __help__ method if it exists...
576 res = ''
577 if hasattr(obj, '__doc__'):
578 res = str(obj.__doc__) + '\n\n'
579 if hasattr(obj, '__help__'):
580 res += str(obj.__help__())
581 if res == '':
582 res = 'no documentation present for this object.'
583 return res
584 # Standard XMLRPC methods:
585 # NOTE: these are only available outside of a session...
586 def listMethods(self):
588 This method returns a list of strings, one for each (non-system)
589 method supported by the XML-RPC server
591 return dict.fromkeys(self.__public_methods__ + self.__system_methods__).keys()
592 def methodHelp(self, name):
594 This method returns a documentation string for a method.
596 if name in self.__public_methods__ + self.__system_methods__:
597 return getattr(self, name).__doc__
598 # this is not expected to get implemeted soon...
599 ## def methodSignature(self, name):
600 ## '''
601 ## '''
602 ## if name in self.__public_methods__:
603 ## return
607 #=======================================================================
608 # vim:set ts=4 sw=4 nowrap :