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',) |
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',) |