cubicweb/hooks/syncsession.py
changeset 11699 b48020a80dc3
parent 11348 70337ad23145
child 11700 41ddaf6802f0
equal deleted inserted replaced
11698:9ea50837bc58 11699:b48020a80dc3
    21 
    21 
    22 from cubicweb import _
    22 from cubicweb import _
    23 from cubicweb import UnknownProperty, BadConnectionId, validation_error
    23 from cubicweb import UnknownProperty, BadConnectionId, validation_error
    24 from cubicweb.predicates import is_instance
    24 from cubicweb.predicates import is_instance
    25 from cubicweb.server import hook
    25 from cubicweb.server import hook
       
    26 from cubicweb.entities.authobjs import user_session_cache_key
    26 
    27 
    27 
    28 
    28 def get_user_sessions(repo, ueid):
    29 def get_user_sessions(repo, ueid):
    29     for session in repo._sessions.values():
    30     for session in repo._sessions.values():
    30         if ueid == session.user.eid:
    31         if ueid == session.user.eid:
    31             yield session
    32             yield session
    32 
    33 
    33 
    34 
       
    35 class CachedValueMixin(object):
       
    36     """Mixin class providing methods to retrieve some value, specified through
       
    37     `value_name` attribute, in session data.
       
    38     """
       
    39     value_name = None
       
    40     session = None  # make pylint happy
       
    41 
       
    42     @property
       
    43     def cached_value(self):
       
    44         """Return cached value for the user, or None"""
       
    45         key = user_session_cache_key(self.session.user.eid, self.value_name)
       
    46         return self.session.data.get(key, None)
       
    47 
       
    48     def update_cached_value(self, value):
       
    49         """Update cached value for the user (modifying the set returned by cached_value may not be
       
    50         necessary depending on session data implementation, e.g. redis)
       
    51         """
       
    52         key = user_session_cache_key(self.session.user.eid, self.value_name)
       
    53         self.session.data[key] = value
       
    54 
       
    55 
    34 class SyncSessionHook(hook.Hook):
    56 class SyncSessionHook(hook.Hook):
    35     __abstract__ = True
    57     __abstract__ = True
    36     category = 'syncsession'
    58     category = 'syncsession'
    37 
    59 
    38 
    60 
    39 # user/groups synchronisation #################################################
    61 # user/groups synchronisation #################################################
    40 
    62 
    41 class _GroupOperation(hook.Operation):
    63 class _GroupOperation(CachedValueMixin, hook.Operation):
    42     """base class for group operation"""
    64     """Base class for group operation"""
    43     cnxuser = None # make pylint happy
    65     value_name = 'groups'
    44 
    66 
    45     def __init__(self, cnx, *args, **kwargs):
    67     def __init__(self, cnx, *args, **kwargs):
    46         """override to get the group name before actual groups manipulation:
    68         """Override to get the group name before actual groups manipulation
    47 
    69 
    48         we may temporarily loose right access during a commit event, so
    70         we may temporarily loose right access during a commit event, so
    49         no query should be emitted while comitting
    71         no query should be emitted while comitting
    50         """
    72         """
    51         rql = 'Any N WHERE G eid %(x)s, G name N'
    73         rql = 'Any N WHERE G eid %(x)s, G name N'
    52         result = cnx.execute(rql, {'x': kwargs['geid']}, build_descr=False)
    74         result = cnx.execute(rql, {'x': kwargs['group_eid']}, build_descr=False)
    53         hook.Operation.__init__(self, cnx, *args, **kwargs)
    75         hook.Operation.__init__(self, cnx, *args, **kwargs)
    54         self.group = result[0][0]
    76         self.group = result[0][0]
    55 
    77 
    56 
    78 
    57 class _DeleteGroupOp(_GroupOperation):
    79 class _DeleteGroupOp(_GroupOperation):
    58     """Synchronize user when a in_group relation has been deleted"""
    80     """Synchronize user when a in_group relation has been deleted"""
    59 
    81 
    60     def postcommit_event(self):
    82     def postcommit_event(self):
    61         """the observed connections set has been commited"""
    83         cached_groups = self.cached_value
    62         groups = self.cnxuser.groups
    84         if cached_groups is not None:
    63         try:
    85             cached_groups.remove(self.group)
    64             groups.remove(self.group)
    86             self.update_cached_value(cached_groups)
    65         except KeyError:
       
    66             self.error('user %s not in group %s',  self.cnxuser, self.group)
       
    67 
    87 
    68 
    88 
    69 class _AddGroupOp(_GroupOperation):
    89 class _AddGroupOp(_GroupOperation):
    70     """Synchronize user when a in_group relation has been added"""
    90     """Synchronize user when a in_group relation has been added"""
    71 
    91 
    72     def postcommit_event(self):
    92     def postcommit_event(self):
    73         """the observed connections set has been commited"""
    93         cached_groups = self.cached_value
    74         groups = self.cnxuser.groups
    94         if cached_groups is not None:
    75         if self.group in groups:
    95             cached_groups.add(self.group)
    76             self.warning('user %s already in group %s', self.cnxuser,
    96             self.update_cached_value(cached_groups)
    77                          self.group)
       
    78         else:
       
    79             groups.add(self.group)
       
    80 
    97 
    81 
    98 
    82 class SyncInGroupHook(SyncSessionHook):
    99 class SyncInGroupHook(SyncSessionHook):
    83     """Watch addition/removal of in_group relation to synchronize living sessions accordingly"""
   100     """Watch addition/removal of in_group relation to synchronize living sessions accordingly"""
    84     __regid__ = 'syncingroup'
   101     __regid__ = 'syncingroup'
    89         if self.event == 'after_delete_relation':
   106         if self.event == 'after_delete_relation':
    90             opcls = _DeleteGroupOp
   107             opcls = _DeleteGroupOp
    91         else:
   108         else:
    92             opcls = _AddGroupOp
   109             opcls = _AddGroupOp
    93         for session in get_user_sessions(self._cw.repo, self.eidfrom):
   110         for session in get_user_sessions(self._cw.repo, self.eidfrom):
    94             opcls(self._cw, cnxuser=session.user, geid=self.eidto)
   111             opcls(self._cw, session=session, group_eid=self.eidto)
    95 
   112 
    96 
   113 
    97 class _DelUserOp(hook.Operation):
   114 class _CloseSessionOp(hook.Operation):
    98     """close associated user's session when it is deleted"""
   115     """Close user's session when it has been deleted"""
    99     def __init__(self, cnx, sessionid):
       
   100         self.sessionid = sessionid
       
   101         hook.Operation.__init__(self, cnx)
       
   102 
   116 
   103     def postcommit_event(self):
   117     def postcommit_event(self):
   104         try:
   118         try:
   105             self.cnx.repo.close(self.sessionid)
   119             # remove cached groups for the user
       
   120             key = user_session_cache_key(self.session.user.eid, 'groups')
       
   121             self.session.data.pop(key, None)
       
   122             self.session.repo.close(self.session.sessionid)
   106         except BadConnectionId:
   123         except BadConnectionId:
   107             pass  # already closed
   124             pass  # already closed
   108 
   125 
   109 
   126 
   110 class CloseDeletedUserSessionsHook(SyncSessionHook):
   127 class UserDeletedHook(SyncSessionHook):
       
   128     """Watch deletion of user to close its opened session"""
   111     __regid__ = 'closession'
   129     __regid__ = 'closession'
   112     __select__ = SyncSessionHook.__select__ & is_instance('CWUser')
   130     __select__ = SyncSessionHook.__select__ & is_instance('CWUser')
   113     events = ('after_delete_entity',)
   131     events = ('after_delete_entity',)
   114 
   132 
   115     def __call__(self):
   133     def __call__(self):
   116         for session in get_user_sessions(self._cw.repo, self.entity.eid):
   134         for session in get_user_sessions(self._cw.repo, self.entity.eid):
   117             _DelUserOp(self._cw, session.sessionid)
   135             _CloseSessionOp(self._cw, session=session)
   118 
   136 
   119 
   137 
   120 # CWProperty hooks #############################################################
   138 # CWProperty hooks #############################################################
   121 
   139 
   122 class _DelCWPropertyOp(hook.Operation):
   140 
   123     """a user's custom properties has been deleted"""
   141 class _UserPropertyOperation(CachedValueMixin, hook.Operation):
   124     cwpropdict = key = None # make pylint happy
   142     """Base class for property operation"""
   125 
   143     value_name = 'properties'
   126     def postcommit_event(self):
   144     key = None  # make pylint happy
   127         """the observed connections set has been commited"""
   145 
   128         try:
   146 
   129             del self.cwpropdict[self.key]
   147 class _ChangeUserCWPropertyOp(_UserPropertyOperation):
   130         except KeyError:
   148     """Synchronize cached user's properties when one has been added/updated"""
   131             self.error('%s has no associated value', self.key)
   149     value = None  # make pylint happy
   132 
   150 
   133 
   151     def postcommit_event(self):
   134 class _ChangeCWPropertyOp(hook.Operation):
   152         cached_props = self.cached_value
   135     """a user's custom properties has been added/changed"""
   153         if cached_props is not None:
   136     cwpropdict = key = value = None # make pylint happy
   154             cached_props[self.key] = self.value
   137 
   155             self.update_cached_value(cached_props)
   138     def postcommit_event(self):
   156 
   139         """the observed connections set has been commited"""
   157 
   140         self.cwpropdict[self.key] = self.value
   158 class _DelUserCWPropertyOp(_UserPropertyOperation):
   141 
   159     """Synchronize cached user's properties when one has been deleted"""
   142 
   160 
   143 class _AddCWPropertyOp(hook.Operation):
   161     def postcommit_event(self):
   144     """a user's custom properties has been added/changed"""
   162         cached_props = self.cached_value
   145     cwprop = None # make pylint happy
   163         if cached_props is not None:
   146 
   164             cached_props.pop(self.key, None)
   147     def postcommit_event(self):
   165             self.update_cached_value(cached_props)
   148         """the observed connections set has been commited"""
   166 
       
   167 
       
   168 class _ChangeSiteWideCWPropertyOp(hook.Operation):
       
   169     """Synchronize site wide properties when one has been added/updated"""
       
   170     cwprop = None  # make pylint happy
       
   171 
       
   172     def postcommit_event(self):
   149         cwprop = self.cwprop
   173         cwprop = self.cwprop
   150         if not cwprop.for_user:
   174         if not cwprop.for_user:
   151             self.cnx.vreg['propertyvalues'][cwprop.pkey] = \
   175             self.cnx.vreg['propertyvalues'][cwprop.pkey] = \
   152                 self.cnx.vreg.typed_value(cwprop.pkey, cwprop.value)
   176                 self.cnx.vreg.typed_value(cwprop.pkey, cwprop.value)
   153         # if for_user is set, update is handled by a ChangeCWPropertyOp operation
   177         # if for_user is set, update is handled by a ChangeUserCWPropertyOp operation
       
   178 
       
   179 
       
   180 class _DelSiteWideCWPropertyOp(hook.Operation):
       
   181     """Synchronize site wide properties when one has been deleted"""
       
   182     key = None  # make pylint happy
       
   183 
       
   184     def postcommit_event(self):
       
   185         self.cnx.vreg['propertyvalues'].pop(self.key, None)
   154 
   186 
   155 
   187 
   156 class AddCWPropertyHook(SyncSessionHook):
   188 class AddCWPropertyHook(SyncSessionHook):
   157     __regid__ = 'addcwprop'
   189     __regid__ = 'addcwprop'
   158     __select__ = SyncSessionHook.__select__ & is_instance('CWProperty')
   190     __select__ = SyncSessionHook.__select__ & is_instance('CWProperty')
   167             value = cnx.vreg.typed_value(key, value)
   199             value = cnx.vreg.typed_value(key, value)
   168         except UnknownProperty:
   200         except UnknownProperty:
   169             msg = _('unknown property key %s')
   201             msg = _('unknown property key %s')
   170             raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
   202             raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
   171         except ValueError as ex:
   203         except ValueError as ex:
   172             raise validation_error(self.entity,
   204             raise validation_error(self.entity, {('value', 'subject'): str(ex)})
   173                                   {('value', 'subject'): str(ex)})
   205         if cnx.user.matching_groups('managers'):
   174         if not cnx.user.matching_groups('managers'):
   206             _ChangeSiteWideCWPropertyOp(cnx, cwprop=self.entity)
       
   207         else:
   175             cnx.add_relation(self.entity.eid, 'for_user', cnx.user.eid)
   208             cnx.add_relation(self.entity.eid, 'for_user', cnx.user.eid)
   176         else:
       
   177             _AddCWPropertyOp(cnx, cwprop=self.entity)
       
   178 
   209 
   179 
   210 
   180 class UpdateCWPropertyHook(AddCWPropertyHook):
   211 class UpdateCWPropertyHook(AddCWPropertyHook):
   181     __regid__ = 'updatecwprop'
   212     __regid__ = 'updatecwprop'
   182     events = ('after_update_entity',)
   213     events = ('after_update_entity',)
   196             return
   227             return
   197         except ValueError as ex:
   228         except ValueError as ex:
   198             raise validation_error(entity, {('value', 'subject'): str(ex)})
   229             raise validation_error(entity, {('value', 'subject'): str(ex)})
   199         if entity.for_user:
   230         if entity.for_user:
   200             for session in get_user_sessions(cnx.repo, entity.for_user[0].eid):
   231             for session in get_user_sessions(cnx.repo, entity.for_user[0].eid):
   201                 _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties,
   232                 _ChangeUserCWPropertyOp(cnx, session=session, key=key, value=value)
   202                                     key=key, value=value)
   233         else:
   203         else:
   234             _ChangeSiteWideCWPropertyOp(cnx, cwprop=self.entity)
   204             # site wide properties
       
   205             _ChangeCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'],
       
   206                               key=key, value=value)
       
   207 
   235 
   208 
   236 
   209 class DeleteCWPropertyHook(AddCWPropertyHook):
   237 class DeleteCWPropertyHook(AddCWPropertyHook):
   210     __regid__ = 'delcwprop'
   238     __regid__ = 'delcwprop'
   211     events = ('before_delete_entity',)
   239     events = ('before_delete_entity',)
   215         for eidfrom, rtype, eidto in cnx.transaction_data.get('pendingrelations', ()):
   243         for eidfrom, rtype, eidto in cnx.transaction_data.get('pendingrelations', ()):
   216             if rtype == 'for_user' and eidfrom == self.entity.eid:
   244             if rtype == 'for_user' and eidfrom == self.entity.eid:
   217                 # if for_user was set, delete already handled by hook on for_user deletion
   245                 # if for_user was set, delete already handled by hook on for_user deletion
   218                 break
   246                 break
   219         else:
   247         else:
   220             _DelCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'],
   248             _DelSiteWideCWPropertyOp(cnx, key=self.entity.pkey)
   221                              key=self.entity.pkey)
       
   222 
   249 
   223 
   250 
   224 class AddForUserRelationHook(SyncSessionHook):
   251 class AddForUserRelationHook(SyncSessionHook):
   225     __regid__ = 'addcwpropforuser'
   252     __regid__ = 'addcwpropforuser'
   226     __select__ = SyncSessionHook.__select__ & hook.match_rtype('for_user')
   253     __select__ = SyncSessionHook.__select__ & hook.match_rtype('for_user')
   235                                  {'x': eidfrom})[0]
   262                                  {'x': eidfrom})[0]
   236         if cnx.vreg.property_info(key)['sitewide']:
   263         if cnx.vreg.property_info(key)['sitewide']:
   237             msg = _("site-wide property can't be set for user")
   264             msg = _("site-wide property can't be set for user")
   238             raise validation_error(eidfrom, {('for_user', 'subject'): msg})
   265             raise validation_error(eidfrom, {('for_user', 'subject'): msg})
   239         for session in get_user_sessions(cnx.repo, self.eidto):
   266         for session in get_user_sessions(cnx.repo, self.eidto):
   240             _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties,
   267             _ChangeUserCWPropertyOp(cnx, session=session, key=key, value=value)
   241                               key=key, value=value)
       
   242 
   268 
   243 
   269 
   244 class DelForUserRelationHook(AddForUserRelationHook):
   270 class DelForUserRelationHook(AddForUserRelationHook):
   245     __regid__ = 'delcwpropforuser'
   271     __regid__ = 'delcwpropforuser'
   246     events = ('after_delete_relation',)
   272     events = ('after_delete_relation',)
   249         cnx = self._cw
   275         cnx = self._cw
   250         key = cnx.execute('Any K WHERE P eid %(x)s, P pkey K', {'x': self.eidfrom})[0][0]
   276         key = cnx.execute('Any K WHERE P eid %(x)s, P pkey K', {'x': self.eidfrom})[0][0]
   251         cnx.transaction_data.setdefault('pendingrelations', []).append(
   277         cnx.transaction_data.setdefault('pendingrelations', []).append(
   252             (self.eidfrom, self.rtype, self.eidto))
   278             (self.eidfrom, self.rtype, self.eidto))
   253         for session in get_user_sessions(cnx.repo, self.eidto):
   279         for session in get_user_sessions(cnx.repo, self.eidto):
   254             _DelCWPropertyOp(cnx, cwpropdict=session.user.properties, key=key)
   280             _DelUserCWPropertyOp(cnx, session=session, key=key)