cubicweb/hooks/syncsession.py
branch3.25
changeset 12126 be8636d12afd
parent 12027 c62c80f20a82
equal deleted inserted replaced
12125:1d3a9bb46339 12126:be8636d12afd
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Core hooks: synchronize living session on persistent data changes"""
    18 """Core hooks: synchronize living session on persistent data changes"""
    19 
    19 
    20 from cubicweb import _
    20 from cubicweb import _
    21 from cubicweb import UnknownProperty, BadConnectionId, validation_error
    21 from cubicweb import UnknownProperty, validation_error
    22 from cubicweb.predicates import is_instance
    22 from cubicweb.predicates import is_instance
    23 from cubicweb.server import hook
    23 from cubicweb.server import hook
    24 from cubicweb.entities.authobjs import user_session_cache_key
       
    25 
       
    26 
       
    27 def get_user_sessions(cnx, user_eid):
       
    28     if cnx.user.eid == user_eid:
       
    29         yield cnx
       
    30 
       
    31 
       
    32 class CachedValueMixin(object):
       
    33     """Mixin class providing methods to retrieve some value, specified through
       
    34     `value_name` attribute, in session data.
       
    35     """
       
    36     value_name = None
       
    37     session = None  # make pylint happy
       
    38 
       
    39     @property
       
    40     def cached_value(self):
       
    41         """Return cached value for the user, or None"""
       
    42         key = user_session_cache_key(self.session.user.eid, self.value_name)
       
    43         return self.session.data.get(key, None)
       
    44 
       
    45     def update_cached_value(self, value):
       
    46         """Update cached value for the user (modifying the set returned by cached_value may not be
       
    47         necessary depending on session data implementation, e.g. redis)
       
    48         """
       
    49         key = user_session_cache_key(self.session.user.eid, self.value_name)
       
    50         self.session.data[key] = value
       
    51 
    24 
    52 
    25 
    53 class SyncSessionHook(hook.Hook):
    26 class SyncSessionHook(hook.Hook):
    54     __abstract__ = True
    27     __abstract__ = True
    55     category = 'syncsession'
    28     category = 'syncsession'
    56 
       
    57 
       
    58 # user/groups synchronisation #################################################
       
    59 
       
    60 class _GroupOperation(CachedValueMixin, hook.Operation):
       
    61     """Base class for group operation"""
       
    62     value_name = 'groups'
       
    63 
       
    64     def __init__(self, cnx, *args, **kwargs):
       
    65         """Override to get the group name before actual groups manipulation
       
    66 
       
    67         we may temporarily loose right access during a commit event, so
       
    68         no query should be emitted while comitting
       
    69         """
       
    70         rql = 'Any N WHERE G eid %(x)s, G name N'
       
    71         result = cnx.execute(rql, {'x': kwargs['group_eid']}, build_descr=False)
       
    72         hook.Operation.__init__(self, cnx, *args, **kwargs)
       
    73         self.group = result[0][0]
       
    74 
       
    75 
       
    76 class _DeleteGroupOp(_GroupOperation):
       
    77     """Synchronize user when a in_group relation has been deleted"""
       
    78 
       
    79     def postcommit_event(self):
       
    80         cached_groups = self.cached_value
       
    81         if cached_groups is not None:
       
    82             cached_groups.remove(self.group)
       
    83             self.update_cached_value(cached_groups)
       
    84 
       
    85 
       
    86 class _AddGroupOp(_GroupOperation):
       
    87     """Synchronize user when a in_group relation has been added"""
       
    88 
       
    89     def postcommit_event(self):
       
    90         cached_groups = self.cached_value
       
    91         if cached_groups is not None:
       
    92             cached_groups.add(self.group)
       
    93             self.update_cached_value(cached_groups)
       
    94 
       
    95 
       
    96 class SyncInGroupHook(SyncSessionHook):
       
    97     """Watch addition/removal of in_group relation to synchronize living sessions accordingly"""
       
    98     __regid__ = 'syncingroup'
       
    99     __select__ = SyncSessionHook.__select__ & hook.match_rtype('in_group')
       
   100     events = ('after_delete_relation', 'after_add_relation')
       
   101 
       
   102     def __call__(self):
       
   103         if self.event == 'after_delete_relation':
       
   104             opcls = _DeleteGroupOp
       
   105         else:
       
   106             opcls = _AddGroupOp
       
   107         for session in get_user_sessions(self._cw, self.eidfrom):
       
   108             opcls(self._cw, session=session, group_eid=self.eidto)
       
   109 
       
   110 
       
   111 class _CloseSessionOp(hook.Operation):
       
   112     """Close user's session when it has been deleted"""
       
   113 
       
   114     def postcommit_event(self):
       
   115         try:
       
   116             # remove cached groups for the user
       
   117             key = user_session_cache_key(self.session.user.eid, 'groups')
       
   118             self.session.data.pop(key, None)
       
   119         except BadConnectionId:
       
   120             pass  # already closed
       
   121 
       
   122 
       
   123 class UserDeletedHook(SyncSessionHook):
       
   124     """Watch deletion of user to close its opened session"""
       
   125     __regid__ = 'closession'
       
   126     __select__ = SyncSessionHook.__select__ & is_instance('CWUser')
       
   127     events = ('after_delete_entity',)
       
   128 
       
   129     def __call__(self):
       
   130         for session in get_user_sessions(self._cw, self.entity.eid):
       
   131             _CloseSessionOp(self._cw, session=session)
       
   132 
       
   133 
       
   134 # CWProperty hooks #############################################################
       
   135 
       
   136 
       
   137 class _UserPropertyOperation(CachedValueMixin, hook.Operation):
       
   138     """Base class for property operation"""
       
   139     value_name = 'properties'
       
   140     key = None  # make pylint happy
       
   141 
       
   142 
       
   143 class _ChangeUserCWPropertyOp(_UserPropertyOperation):
       
   144     """Synchronize cached user's properties when one has been added/updated"""
       
   145     value = None  # make pylint happy
       
   146 
       
   147     def postcommit_event(self):
       
   148         cached_props = self.cached_value
       
   149         if cached_props is not None:
       
   150             cached_props[self.key] = self.value
       
   151             self.update_cached_value(cached_props)
       
   152 
       
   153 
       
   154 class _DelUserCWPropertyOp(_UserPropertyOperation):
       
   155     """Synchronize cached user's properties when one has been deleted"""
       
   156 
       
   157     def postcommit_event(self):
       
   158         cached_props = self.cached_value
       
   159         if cached_props is not None:
       
   160             cached_props.pop(self.key, None)
       
   161             self.update_cached_value(cached_props)
       
   162 
    29 
   163 
    30 
   164 class _ChangeSiteWideCWPropertyOp(hook.Operation):
    31 class _ChangeSiteWideCWPropertyOp(hook.Operation):
   165     """Synchronize site wide properties when one has been added/updated"""
    32     """Synchronize site wide properties when one has been added/updated"""
   166     cwprop = None  # make pylint happy
    33     cwprop = None  # make pylint happy
   221             value = cnx.vreg.typed_value(key, value)
    88             value = cnx.vreg.typed_value(key, value)
   222         except UnknownProperty:
    89         except UnknownProperty:
   223             return
    90             return
   224         except ValueError as ex:
    91         except ValueError as ex:
   225             raise validation_error(entity, {('value', 'subject'): str(ex)})
    92             raise validation_error(entity, {('value', 'subject'): str(ex)})
   226         if entity.for_user:
    93         if not entity.for_user:
   227             for session in get_user_sessions(cnx, entity.for_user[0].eid):
       
   228                 _ChangeUserCWPropertyOp(cnx, session=session, key=key, value=value)
       
   229         else:
       
   230             _ChangeSiteWideCWPropertyOp(cnx, cwprop=self.entity)
    94             _ChangeSiteWideCWPropertyOp(cnx, cwprop=self.entity)
   231 
    95 
   232 
    96 
   233 class DeleteCWPropertyHook(AddCWPropertyHook):
    97 class DeleteCWPropertyHook(AddCWPropertyHook):
   234     __regid__ = 'delcwprop'
    98     __regid__ = 'delcwprop'
   236 
   100 
   237     def __call__(self):
   101     def __call__(self):
   238         cnx = self._cw
   102         cnx = self._cw
   239         for eidfrom, rtype, eidto in cnx.transaction_data.get('pendingrelations', ()):
   103         for eidfrom, rtype, eidto in cnx.transaction_data.get('pendingrelations', ()):
   240             if rtype == 'for_user' and eidfrom == self.entity.eid:
   104             if rtype == 'for_user' and eidfrom == self.entity.eid:
   241                 # if for_user was set, delete already handled by hook on for_user deletion
   105                 # not need to sync user specific properties
   242                 break
   106                 break
   243         else:
   107         else:
   244             _DelSiteWideCWPropertyOp(cnx, key=self.entity.pkey)
   108             _DelSiteWideCWPropertyOp(cnx, key=self.entity.pkey)
   245 
   109 
   246 
   110 
   257         key, value = cnx.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
   121         key, value = cnx.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
   258                                  {'x': eidfrom})[0]
   122                                  {'x': eidfrom})[0]
   259         if cnx.vreg.property_info(key)['sitewide']:
   123         if cnx.vreg.property_info(key)['sitewide']:
   260             msg = _("site-wide property can't be set for user")
   124             msg = _("site-wide property can't be set for user")
   261             raise validation_error(eidfrom, {('for_user', 'subject'): msg})
   125             raise validation_error(eidfrom, {('for_user', 'subject'): msg})
   262         for session in get_user_sessions(cnx, self.eidto):
       
   263             _ChangeUserCWPropertyOp(cnx, session=session, key=key, value=value)
       
   264 
   126 
   265 
   127 
   266 class DelForUserRelationHook(AddForUserRelationHook):
   128 class DelForUserRelationHook(AddForUserRelationHook):
   267     __regid__ = 'delcwpropforuser'
   129     __regid__ = 'delcwpropforuser'
   268     events = ('after_delete_relation',)
   130     events = ('after_delete_relation',)
   269 
   131 
   270     def __call__(self):
   132     def __call__(self):
   271         cnx = self._cw
   133         cnx = self._cw
   272         key = cnx.execute('Any K WHERE P eid %(x)s, P pkey K', {'x': self.eidfrom})[0][0]
       
   273         cnx.transaction_data.setdefault('pendingrelations', []).append(
   134         cnx.transaction_data.setdefault('pendingrelations', []).append(
   274             (self.eidfrom, self.rtype, self.eidto))
   135             (self.eidfrom, self.rtype, self.eidto))
   275         for session in get_user_sessions(cnx, self.eidto):
       
   276             _DelUserCWPropertyOp(cnx, session=session, key=key)