cubicweb/hooks/syncsession.py
changeset 11699 b48020a80dc3
parent 11348 70337ad23145
child 11700 41ddaf6802f0
--- a/cubicweb/hooks/syncsession.py	Wed Oct 05 10:17:39 2016 +0200
+++ b/cubicweb/hooks/syncsession.py	Mon Jun 06 15:26:49 2016 +0200
@@ -23,6 +23,7 @@
 from cubicweb import UnknownProperty, BadConnectionId, validation_error
 from cubicweb.predicates import is_instance
 from cubicweb.server import hook
+from cubicweb.entities.authobjs import user_session_cache_key
 
 
 def get_user_sessions(repo, ueid):
@@ -31,6 +32,27 @@
             yield session
 
 
+class CachedValueMixin(object):
+    """Mixin class providing methods to retrieve some value, specified through
+    `value_name` attribute, in session data.
+    """
+    value_name = None
+    session = None  # make pylint happy
+
+    @property
+    def cached_value(self):
+        """Return cached value for the user, or None"""
+        key = user_session_cache_key(self.session.user.eid, self.value_name)
+        return self.session.data.get(key, None)
+
+    def update_cached_value(self, value):
+        """Update cached value for the user (modifying the set returned by cached_value may not be
+        necessary depending on session data implementation, e.g. redis)
+        """
+        key = user_session_cache_key(self.session.user.eid, self.value_name)
+        self.session.data[key] = value
+
+
 class SyncSessionHook(hook.Hook):
     __abstract__ = True
     category = 'syncsession'
@@ -38,18 +60,18 @@
 
 # user/groups synchronisation #################################################
 
-class _GroupOperation(hook.Operation):
-    """base class for group operation"""
-    cnxuser = None # make pylint happy
+class _GroupOperation(CachedValueMixin, hook.Operation):
+    """Base class for group operation"""
+    value_name = 'groups'
 
     def __init__(self, cnx, *args, **kwargs):
-        """override to get the group name before actual groups manipulation:
+        """Override to get the group name before actual groups manipulation
 
         we may temporarily loose right access during a commit event, so
         no query should be emitted while comitting
         """
         rql = 'Any N WHERE G eid %(x)s, G name N'
-        result = cnx.execute(rql, {'x': kwargs['geid']}, build_descr=False)
+        result = cnx.execute(rql, {'x': kwargs['group_eid']}, build_descr=False)
         hook.Operation.__init__(self, cnx, *args, **kwargs)
         self.group = result[0][0]
 
@@ -58,25 +80,20 @@
     """Synchronize user when a in_group relation has been deleted"""
 
     def postcommit_event(self):
-        """the observed connections set has been commited"""
-        groups = self.cnxuser.groups
-        try:
-            groups.remove(self.group)
-        except KeyError:
-            self.error('user %s not in group %s',  self.cnxuser, self.group)
+        cached_groups = self.cached_value
+        if cached_groups is not None:
+            cached_groups.remove(self.group)
+            self.update_cached_value(cached_groups)
 
 
 class _AddGroupOp(_GroupOperation):
     """Synchronize user when a in_group relation has been added"""
 
     def postcommit_event(self):
-        """the observed connections set has been commited"""
-        groups = self.cnxuser.groups
-        if self.group in groups:
-            self.warning('user %s already in group %s', self.cnxuser,
-                         self.group)
-        else:
-            groups.add(self.group)
+        cached_groups = self.cached_value
+        if cached_groups is not None:
+            cached_groups.add(self.group)
+            self.update_cached_value(cached_groups)
 
 
 class SyncInGroupHook(SyncSessionHook):
@@ -91,66 +108,81 @@
         else:
             opcls = _AddGroupOp
         for session in get_user_sessions(self._cw.repo, self.eidfrom):
-            opcls(self._cw, cnxuser=session.user, geid=self.eidto)
+            opcls(self._cw, session=session, group_eid=self.eidto)
 
 
-class _DelUserOp(hook.Operation):
-    """close associated user's session when it is deleted"""
-    def __init__(self, cnx, sessionid):
-        self.sessionid = sessionid
-        hook.Operation.__init__(self, cnx)
+class _CloseSessionOp(hook.Operation):
+    """Close user's session when it has been deleted"""
 
     def postcommit_event(self):
         try:
-            self.cnx.repo.close(self.sessionid)
+            # remove cached groups for the user
+            key = user_session_cache_key(self.session.user.eid, 'groups')
+            self.session.data.pop(key, None)
+            self.session.repo.close(self.session.sessionid)
         except BadConnectionId:
             pass  # already closed
 
 
-class CloseDeletedUserSessionsHook(SyncSessionHook):
+class UserDeletedHook(SyncSessionHook):
+    """Watch deletion of user to close its opened session"""
     __regid__ = 'closession'
     __select__ = SyncSessionHook.__select__ & is_instance('CWUser')
     events = ('after_delete_entity',)
 
     def __call__(self):
         for session in get_user_sessions(self._cw.repo, self.entity.eid):
-            _DelUserOp(self._cw, session.sessionid)
+            _CloseSessionOp(self._cw, session=session)
 
 
 # CWProperty hooks #############################################################
 
-class _DelCWPropertyOp(hook.Operation):
-    """a user's custom properties has been deleted"""
-    cwpropdict = key = None # make pylint happy
 
-    def postcommit_event(self):
-        """the observed connections set has been commited"""
-        try:
-            del self.cwpropdict[self.key]
-        except KeyError:
-            self.error('%s has no associated value', self.key)
+class _UserPropertyOperation(CachedValueMixin, hook.Operation):
+    """Base class for property operation"""
+    value_name = 'properties'
+    key = None  # make pylint happy
 
 
-class _ChangeCWPropertyOp(hook.Operation):
-    """a user's custom properties has been added/changed"""
-    cwpropdict = key = value = None # make pylint happy
+class _ChangeUserCWPropertyOp(_UserPropertyOperation):
+    """Synchronize cached user's properties when one has been added/updated"""
+    value = None  # make pylint happy
 
     def postcommit_event(self):
-        """the observed connections set has been commited"""
-        self.cwpropdict[self.key] = self.value
+        cached_props = self.cached_value
+        if cached_props is not None:
+            cached_props[self.key] = self.value
+            self.update_cached_value(cached_props)
 
 
-class _AddCWPropertyOp(hook.Operation):
-    """a user's custom properties has been added/changed"""
-    cwprop = None # make pylint happy
+class _DelUserCWPropertyOp(_UserPropertyOperation):
+    """Synchronize cached user's properties when one has been deleted"""
 
     def postcommit_event(self):
-        """the observed connections set has been commited"""
+        cached_props = self.cached_value
+        if cached_props is not None:
+            cached_props.pop(self.key, None)
+            self.update_cached_value(cached_props)
+
+
+class _ChangeSiteWideCWPropertyOp(hook.Operation):
+    """Synchronize site wide properties when one has been added/updated"""
+    cwprop = None  # make pylint happy
+
+    def postcommit_event(self):
         cwprop = self.cwprop
         if not cwprop.for_user:
             self.cnx.vreg['propertyvalues'][cwprop.pkey] = \
                 self.cnx.vreg.typed_value(cwprop.pkey, cwprop.value)
-        # if for_user is set, update is handled by a ChangeCWPropertyOp operation
+        # if for_user is set, update is handled by a ChangeUserCWPropertyOp operation
+
+
+class _DelSiteWideCWPropertyOp(hook.Operation):
+    """Synchronize site wide properties when one has been deleted"""
+    key = None  # make pylint happy
+
+    def postcommit_event(self):
+        self.cnx.vreg['propertyvalues'].pop(self.key, None)
 
 
 class AddCWPropertyHook(SyncSessionHook):
@@ -169,12 +201,11 @@
             msg = _('unknown property key %s')
             raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
         except ValueError as ex:
-            raise validation_error(self.entity,
-                                  {('value', 'subject'): str(ex)})
-        if not cnx.user.matching_groups('managers'):
+            raise validation_error(self.entity, {('value', 'subject'): str(ex)})
+        if cnx.user.matching_groups('managers'):
+            _ChangeSiteWideCWPropertyOp(cnx, cwprop=self.entity)
+        else:
             cnx.add_relation(self.entity.eid, 'for_user', cnx.user.eid)
-        else:
-            _AddCWPropertyOp(cnx, cwprop=self.entity)
 
 
 class UpdateCWPropertyHook(AddCWPropertyHook):
@@ -198,12 +229,9 @@
             raise validation_error(entity, {('value', 'subject'): str(ex)})
         if entity.for_user:
             for session in get_user_sessions(cnx.repo, entity.for_user[0].eid):
-                _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties,
-                                    key=key, value=value)
+                _ChangeUserCWPropertyOp(cnx, session=session, key=key, value=value)
         else:
-            # site wide properties
-            _ChangeCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'],
-                              key=key, value=value)
+            _ChangeSiteWideCWPropertyOp(cnx, cwprop=self.entity)
 
 
 class DeleteCWPropertyHook(AddCWPropertyHook):
@@ -217,8 +245,7 @@
                 # if for_user was set, delete already handled by hook on for_user deletion
                 break
         else:
-            _DelCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'],
-                             key=self.entity.pkey)
+            _DelSiteWideCWPropertyOp(cnx, key=self.entity.pkey)
 
 
 class AddForUserRelationHook(SyncSessionHook):
@@ -237,8 +264,7 @@
             msg = _("site-wide property can't be set for user")
             raise validation_error(eidfrom, {('for_user', 'subject'): msg})
         for session in get_user_sessions(cnx.repo, self.eidto):
-            _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties,
-                              key=key, value=value)
+            _ChangeUserCWPropertyOp(cnx, session=session, key=key, value=value)
 
 
 class DelForUserRelationHook(AddForUserRelationHook):
@@ -251,4 +277,4 @@
         cnx.transaction_data.setdefault('pendingrelations', []).append(
             (self.eidfrom, self.rtype, self.eidto))
         for session in get_user_sessions(cnx.repo, self.eidto):
-            _DelCWPropertyOp(cnx, cwpropdict=session.user.properties, key=key)
+            _DelUserCWPropertyOp(cnx, session=session, key=key)