1 # copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
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/>. |
|
18 """Repository users' and internal' sessions.""" |
|
19 from __future__ import print_function |
|
20 |
|
21 __docformat__ = "restructuredtext en" |
|
22 |
|
23 import sys |
|
24 from time import time |
|
25 from uuid import uuid4 |
|
26 from warnings import warn |
|
27 import functools |
|
28 from contextlib import contextmanager |
|
29 |
|
30 from six import text_type |
|
31 |
|
32 from logilab.common.deprecation import deprecated |
|
33 from logilab.common.textutils import unormalize |
|
34 from logilab.common.registry import objectify_predicate |
|
35 |
|
36 from cubicweb import QueryError, schema, server, ProgrammingError |
|
37 from cubicweb.req import RequestSessionBase |
|
38 from cubicweb.utils import make_uid |
|
39 from cubicweb.rqlrewrite import RQLRewriter |
|
40 from cubicweb.server.edition import EditedEntity |
|
41 |
|
42 |
|
43 NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy() |
|
44 NO_UNDO_TYPES.add('CWCache') |
|
45 # is / is_instance_of are usually added by sql hooks except when using |
|
46 # dataimport.NoHookRQLObjectStore, and we don't want to record them |
|
47 # anyway in the later case |
|
48 NO_UNDO_TYPES.add('is') |
|
49 NO_UNDO_TYPES.add('is_instance_of') |
|
50 NO_UNDO_TYPES.add('cw_source') |
|
51 # XXX rememberme,forgotpwd,apycot,vcsfile |
|
52 |
|
53 @objectify_predicate |
|
54 def is_user_session(cls, req, **kwargs): |
|
55 """return 1 when session is not internal. |
|
56 |
|
57 This predicate can only be used repository side only. """ |
|
58 return not req.is_internal_session |
|
59 |
|
60 @objectify_predicate |
|
61 def is_internal_session(cls, req, **kwargs): |
|
62 """return 1 when session is not internal. |
|
63 |
|
64 This predicate can only be used repository side only. """ |
|
65 return req.is_internal_session |
|
66 |
|
67 @objectify_predicate |
|
68 def repairing(cls, req, **kwargs): |
|
69 """return 1 when repository is running in repair mode""" |
|
70 return req.vreg.config.repairing |
|
71 |
|
72 |
|
73 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead') |
|
74 def hooks_control(obj, mode, *categories): |
|
75 assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL) |
|
76 if mode == HOOKS_ALLOW_ALL: |
|
77 return obj.allow_all_hooks_but(*categories) |
|
78 elif mode == HOOKS_DENY_ALL: |
|
79 return obj.deny_all_hooks_but(*categories) |
|
80 |
|
81 |
|
82 class _hooks_control(object): |
|
83 """context manager to control activated hooks categories. |
|
84 |
|
85 If mode is `HOOKS_DENY_ALL`, given hooks categories will |
|
86 be enabled. |
|
87 |
|
88 If mode is `HOOKS_ALLOW_ALL`, given hooks categories will |
|
89 be disabled. |
|
90 |
|
91 .. sourcecode:: python |
|
92 |
|
93 with _hooks_control(cnx, HOOKS_ALLOW_ALL, 'integrity'): |
|
94 # ... do stuff with all but 'integrity' hooks activated |
|
95 |
|
96 with _hooks_control(cnx, HOOKS_DENY_ALL, 'integrity'): |
|
97 # ... do stuff with none but 'integrity' hooks activated |
|
98 |
|
99 This is an internal API, you should rather use |
|
100 :meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` or |
|
101 :meth:`~cubicweb.server.session.Connection.allow_all_hooks_but` |
|
102 Connection methods. |
|
103 """ |
|
104 def __init__(self, cnx, mode, *categories): |
|
105 assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL) |
|
106 self.cnx = cnx |
|
107 self.mode = mode |
|
108 self.categories = categories |
|
109 self.oldmode = None |
|
110 self.changes = () |
|
111 |
|
112 def __enter__(self): |
|
113 self.oldmode = self.cnx.hooks_mode |
|
114 self.cnx.hooks_mode = self.mode |
|
115 if self.mode is HOOKS_DENY_ALL: |
|
116 self.changes = self.cnx.enable_hook_categories(*self.categories) |
|
117 else: |
|
118 self.changes = self.cnx.disable_hook_categories(*self.categories) |
|
119 self.cnx.ctx_count += 1 |
|
120 |
|
121 def __exit__(self, exctype, exc, traceback): |
|
122 self.cnx.ctx_count -= 1 |
|
123 try: |
|
124 if self.categories: |
|
125 if self.mode is HOOKS_DENY_ALL: |
|
126 self.cnx.disable_hook_categories(*self.categories) |
|
127 else: |
|
128 self.cnx.enable_hook_categories(*self.categories) |
|
129 finally: |
|
130 self.cnx.hooks_mode = self.oldmode |
|
131 |
|
132 |
|
133 @deprecated('[3.17] use <object>.security_enabled instead') |
|
134 def security_enabled(obj, *args, **kwargs): |
|
135 return obj.security_enabled(*args, **kwargs) |
|
136 |
|
137 class _security_enabled(object): |
|
138 """context manager to control security w/ session.execute, |
|
139 |
|
140 By default security is disabled on queries executed on the repository |
|
141 side. |
|
142 """ |
|
143 def __init__(self, cnx, read=None, write=None): |
|
144 self.cnx = cnx |
|
145 self.read = read |
|
146 self.write = write |
|
147 self.oldread = None |
|
148 self.oldwrite = None |
|
149 |
|
150 def __enter__(self): |
|
151 if self.read is None: |
|
152 self.oldread = None |
|
153 else: |
|
154 self.oldread = self.cnx.read_security |
|
155 self.cnx.read_security = self.read |
|
156 if self.write is None: |
|
157 self.oldwrite = None |
|
158 else: |
|
159 self.oldwrite = self.cnx.write_security |
|
160 self.cnx.write_security = self.write |
|
161 self.cnx.ctx_count += 1 |
|
162 |
|
163 def __exit__(self, exctype, exc, traceback): |
|
164 self.cnx.ctx_count -= 1 |
|
165 if self.oldread is not None: |
|
166 self.cnx.read_security = self.oldread |
|
167 if self.oldwrite is not None: |
|
168 self.cnx.write_security = self.oldwrite |
|
169 |
|
170 HOOKS_ALLOW_ALL = object() |
|
171 HOOKS_DENY_ALL = object() |
|
172 DEFAULT_SECURITY = object() # evaluated to true by design |
|
173 |
|
174 class SessionClosedError(RuntimeError): |
|
175 pass |
|
176 |
|
177 |
|
178 def _open_only(func): |
|
179 """decorator for Connection method that check it is open""" |
|
180 @functools.wraps(func) |
|
181 def check_open(cnx, *args, **kwargs): |
|
182 if not cnx._open: |
|
183 raise ProgrammingError('Closed Connection: %s' |
|
184 % cnx.connectionid) |
|
185 return func(cnx, *args, **kwargs) |
|
186 return check_open |
|
187 |
|
188 |
|
189 class Connection(RequestSessionBase): |
|
190 """Repository Connection |
|
191 |
|
192 Holds all connection related data |
|
193 |
|
194 Database connection resources: |
|
195 |
|
196 :attr:`hooks_in_progress`, boolean flag telling if the executing |
|
197 query is coming from a repoapi connection or is a query from |
|
198 within the repository (e.g. started by hooks) |
|
199 |
|
200 :attr:`cnxset`, the connections set to use to execute queries on sources. |
|
201 If the transaction is read only, the connection set may be freed between |
|
202 actual queries. This allows multiple connections with a reasonably low |
|
203 connection set pool size. Control mechanism is detailed below. |
|
204 |
|
205 .. automethod:: cubicweb.server.session.Connection.set_cnxset |
|
206 .. automethod:: cubicweb.server.session.Connection.free_cnxset |
|
207 |
|
208 :attr:`mode`, string telling the connections set handling mode, may be one |
|
209 of 'read' (connections set may be freed), 'write' (some write was done in |
|
210 the connections set, it can't be freed before end of the transaction), |
|
211 'transaction' (we want to keep the connections set during all the |
|
212 transaction, with or without writing) |
|
213 |
|
214 Shared data: |
|
215 |
|
216 :attr:`data` is a dictionary bound to the underlying session, |
|
217 who will be present for the life time of the session. This may |
|
218 be useful for web clients that rely on the server for managing |
|
219 bits of session-scoped data. |
|
220 |
|
221 :attr:`transaction_data` is a dictionary cleared at the end of |
|
222 the transaction. Hooks and operations may put arbitrary data in |
|
223 there. |
|
224 |
|
225 Internal state: |
|
226 |
|
227 :attr:`pending_operations`, ordered list of operations to be processed on |
|
228 commit/rollback |
|
229 |
|
230 :attr:`commit_state`, describing the transaction commit state, may be one |
|
231 of None (not yet committing), 'precommit' (calling precommit event on |
|
232 operations), 'postcommit' (calling postcommit event on operations), |
|
233 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error |
|
234 has been raised during the transaction and so it must be rolled back). |
|
235 |
|
236 Hooks controls: |
|
237 |
|
238 :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. |
|
239 |
|
240 :attr:`enabled_hook_cats`, when :attr:`hooks_mode` is |
|
241 `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. |
|
242 |
|
243 :attr:`disabled_hook_cats`, when :attr:`hooks_mode` is |
|
244 `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. |
|
245 |
|
246 Security level Management: |
|
247 |
|
248 :attr:`read_security` and :attr:`write_security`, boolean flags telling if |
|
249 read/write security is currently activated. |
|
250 |
|
251 """ |
|
252 is_request = False |
|
253 hooks_in_progress = False |
|
254 is_repo_in_memory = True # bw compat |
|
255 |
|
256 def __init__(self, session): |
|
257 # using super(Connection, self) confuse some test hack |
|
258 RequestSessionBase.__init__(self, session.vreg) |
|
259 #: connection unique id |
|
260 self._open = None |
|
261 self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex) |
|
262 self.session = session |
|
263 self.sessionid = session.sessionid |
|
264 #: reentrance handling |
|
265 self.ctx_count = 0 |
|
266 |
|
267 #: server.Repository object |
|
268 self.repo = session.repo |
|
269 self.vreg = self.repo.vreg |
|
270 self._execute = self.repo.querier.execute |
|
271 |
|
272 # other session utility |
|
273 self._session_timestamp = session._timestamp |
|
274 |
|
275 # internal (root) session |
|
276 self.is_internal_session = isinstance(session.user, InternalManager) |
|
277 |
|
278 #: dict containing arbitrary data cleared at the end of the transaction |
|
279 self.transaction_data = {} |
|
280 self._session_data = session.data |
|
281 #: ordered list of operations to be processed on commit/rollback |
|
282 self.pending_operations = [] |
|
283 #: (None, 'precommit', 'postcommit', 'uncommitable') |
|
284 self.commit_state = None |
|
285 |
|
286 ### hook control attribute |
|
287 self.hooks_mode = HOOKS_ALLOW_ALL |
|
288 self.disabled_hook_cats = set() |
|
289 self.enabled_hook_cats = set() |
|
290 self.pruned_hooks_cache = {} |
|
291 |
|
292 |
|
293 ### security control attributes |
|
294 self._read_security = DEFAULT_SECURITY # handled by a property |
|
295 self.write_security = DEFAULT_SECURITY |
|
296 |
|
297 # undo control |
|
298 config = session.repo.config |
|
299 if config.creating or config.repairing or self.is_internal_session: |
|
300 self.undo_actions = False |
|
301 else: |
|
302 self.undo_actions = config['undo-enabled'] |
|
303 |
|
304 # RQLRewriter are not thread safe |
|
305 self._rewriter = RQLRewriter(self) |
|
306 |
|
307 # other session utility |
|
308 if session.user.login == '__internal_manager__': |
|
309 self.user = session.user |
|
310 self.set_language(self.user.prefered_language()) |
|
311 else: |
|
312 self._set_user(session.user) |
|
313 |
|
314 @_open_only |
|
315 def source_defs(self): |
|
316 """Return the definition of sources used by the repository.""" |
|
317 return self.session.repo.source_defs() |
|
318 |
|
319 @_open_only |
|
320 def get_schema(self): |
|
321 """Return the schema currently used by the repository.""" |
|
322 return self.session.repo.source_defs() |
|
323 |
|
324 @_open_only |
|
325 def get_option_value(self, option): |
|
326 """Return the value for `option` in the configuration.""" |
|
327 return self.session.repo.get_option_value(option) |
|
328 |
|
329 # transaction api |
|
330 |
|
331 @_open_only |
|
332 def undoable_transactions(self, ueid=None, **actionfilters): |
|
333 """Return a list of undoable transaction objects by the connection's |
|
334 user, ordered by descendant transaction time. |
|
335 |
|
336 Managers may filter according to user (eid) who has done the transaction |
|
337 using the `ueid` argument. Others will only see their own transactions. |
|
338 |
|
339 Additional filtering capabilities is provided by using the following |
|
340 named arguments: |
|
341 |
|
342 * `etype` to get only transactions creating/updating/deleting entities |
|
343 of the given type |
|
344 |
|
345 * `eid` to get only transactions applied to entity of the given eid |
|
346 |
|
347 * `action` to get only transactions doing the given action (action in |
|
348 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or |
|
349 'D'. |
|
350 |
|
351 * `public`: when additional filtering is provided, they are by default |
|
352 only searched in 'public' actions, unless a `public` argument is given |
|
353 and set to false. |
|
354 """ |
|
355 return self.repo.system_source.undoable_transactions(self, ueid, |
|
356 **actionfilters) |
|
357 |
|
358 @_open_only |
|
359 def transaction_info(self, txuuid): |
|
360 """Return transaction object for the given uid. |
|
361 |
|
362 raise `NoSuchTransaction` if not found or if session's user is |
|
363 not allowed (eg not in managers group and the transaction |
|
364 doesn't belong to him). |
|
365 """ |
|
366 return self.repo.system_source.tx_info(self, txuuid) |
|
367 |
|
368 @_open_only |
|
369 def transaction_actions(self, txuuid, public=True): |
|
370 """Return an ordered list of actions effectued during that transaction. |
|
371 |
|
372 If public is true, return only 'public' actions, i.e. not ones |
|
373 triggered under the cover by hooks, else return all actions. |
|
374 |
|
375 raise `NoSuchTransaction` if the transaction is not found or |
|
376 if the user is not allowed (eg not in managers group). |
|
377 """ |
|
378 return self.repo.system_source.tx_actions(self, txuuid, public) |
|
379 |
|
380 @_open_only |
|
381 def undo_transaction(self, txuuid): |
|
382 """Undo the given transaction. Return potential restoration errors. |
|
383 |
|
384 raise `NoSuchTransaction` if not found or if user is not |
|
385 allowed (eg not in managers group). |
|
386 """ |
|
387 return self.repo.system_source.undo_transaction(self, txuuid) |
|
388 |
|
389 # life cycle handling #################################################### |
|
390 |
|
391 def __enter__(self): |
|
392 assert self._open is None # first opening |
|
393 self._open = True |
|
394 self.cnxset = self.repo._get_cnxset() |
|
395 return self |
|
396 |
|
397 def __exit__(self, exctype=None, excvalue=None, tb=None): |
|
398 assert self._open # actually already open |
|
399 self.rollback() |
|
400 self._open = False |
|
401 self.cnxset.cnxset_freed() |
|
402 self.repo._free_cnxset(self.cnxset) |
|
403 self.cnxset = None |
|
404 |
|
405 @contextmanager |
|
406 def running_hooks_ops(self): |
|
407 """this context manager should be called whenever hooks or operations |
|
408 are about to be run (but after hook selection) |
|
409 |
|
410 It will help the undo logic record pertinent metadata or some |
|
411 hooks to run (or not) depending on who/what issued the query. |
|
412 """ |
|
413 prevmode = self.hooks_in_progress |
|
414 self.hooks_in_progress = True |
|
415 yield |
|
416 self.hooks_in_progress = prevmode |
|
417 |
|
418 # shared data handling ################################################### |
|
419 |
|
420 @property |
|
421 def data(self): |
|
422 return self._session_data |
|
423 |
|
424 @property |
|
425 def rql_rewriter(self): |
|
426 return self._rewriter |
|
427 |
|
428 @_open_only |
|
429 @deprecated('[3.19] use session or transaction data', stacklevel=3) |
|
430 def get_shared_data(self, key, default=None, pop=False, txdata=False): |
|
431 """return value associated to `key` in session data""" |
|
432 if txdata: |
|
433 data = self.transaction_data |
|
434 else: |
|
435 data = self._session_data |
|
436 if pop: |
|
437 return data.pop(key, default) |
|
438 else: |
|
439 return data.get(key, default) |
|
440 |
|
441 @_open_only |
|
442 @deprecated('[3.19] use session or transaction data', stacklevel=3) |
|
443 def set_shared_data(self, key, value, txdata=False): |
|
444 """set value associated to `key` in session data""" |
|
445 if txdata: |
|
446 self.transaction_data[key] = value |
|
447 else: |
|
448 self._session_data[key] = value |
|
449 |
|
450 def clear(self): |
|
451 """reset internal data""" |
|
452 self.transaction_data = {} |
|
453 #: ordered list of operations to be processed on commit/rollback |
|
454 self.pending_operations = [] |
|
455 #: (None, 'precommit', 'postcommit', 'uncommitable') |
|
456 self.commit_state = None |
|
457 self.pruned_hooks_cache = {} |
|
458 self.local_perm_cache.clear() |
|
459 self.rewriter = RQLRewriter(self) |
|
460 |
|
461 @deprecated('[3.19] cnxset are automatically managed now.' |
|
462 ' stop using explicit set and free.') |
|
463 def set_cnxset(self): |
|
464 pass |
|
465 |
|
466 @deprecated('[3.19] cnxset are automatically managed now.' |
|
467 ' stop using explicit set and free.') |
|
468 def free_cnxset(self, ignoremode=False): |
|
469 pass |
|
470 |
|
471 @property |
|
472 @contextmanager |
|
473 @_open_only |
|
474 @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.' |
|
475 ' stop using .ensure_cnx_set') |
|
476 def ensure_cnx_set(self): |
|
477 yield |
|
478 |
|
479 @property |
|
480 def anonymous_connection(self): |
|
481 return self.session.anonymous_session |
|
482 |
|
483 # Entity cache management ################################################# |
|
484 # |
|
485 # The connection entity cache as held in cnx.transaction_data is removed at the |
|
486 # end of the connection (commit and rollback) |
|
487 # |
|
488 # XXX connection level caching may be a pb with multiple repository |
|
489 # instances, but 1. this is probably not the only one :$ and 2. it may be |
|
490 # an acceptable risk. Anyway we could activate it or not according to a |
|
491 # configuration option |
|
492 |
|
493 def set_entity_cache(self, entity): |
|
494 """Add `entity` to the connection entity cache""" |
|
495 # XXX not using _open_only because before at creation time. _set_user |
|
496 # call this function to cache the Connection user. |
|
497 if entity.cw_etype != 'CWUser' and not self._open: |
|
498 raise ProgrammingError('Closed Connection: %s' |
|
499 % self.connectionid) |
|
500 ecache = self.transaction_data.setdefault('ecache', {}) |
|
501 ecache.setdefault(entity.eid, entity) |
|
502 |
|
503 @_open_only |
|
504 def entity_cache(self, eid): |
|
505 """get cache entity for `eid`""" |
|
506 return self.transaction_data['ecache'][eid] |
|
507 |
|
508 @_open_only |
|
509 def cached_entities(self): |
|
510 """return the whole entity cache""" |
|
511 return self.transaction_data.get('ecache', {}).values() |
|
512 |
|
513 @_open_only |
|
514 def drop_entity_cache(self, eid=None): |
|
515 """drop entity from the cache |
|
516 |
|
517 If eid is None, the whole cache is dropped""" |
|
518 if eid is None: |
|
519 self.transaction_data.pop('ecache', None) |
|
520 else: |
|
521 del self.transaction_data['ecache'][eid] |
|
522 |
|
523 # relations handling ####################################################### |
|
524 |
|
525 @_open_only |
|
526 def add_relation(self, fromeid, rtype, toeid): |
|
527 """provide direct access to the repository method to add a relation. |
|
528 |
|
529 This is equivalent to the following rql query: |
|
530 |
|
531 SET X rtype Y WHERE X eid fromeid, T eid toeid |
|
532 |
|
533 without read security check but also all the burden of rql execution. |
|
534 You may use this in hooks when you know both eids of the relation you |
|
535 want to add. |
|
536 """ |
|
537 self.add_relations([(rtype, [(fromeid, toeid)])]) |
|
538 |
|
539 @_open_only |
|
540 def add_relations(self, relations): |
|
541 '''set many relation using a shortcut similar to the one in add_relation |
|
542 |
|
543 relations is a list of 2-uples, the first element of each |
|
544 2-uple is the rtype, and the second is a list of (fromeid, |
|
545 toeid) tuples |
|
546 ''' |
|
547 edited_entities = {} |
|
548 relations_dict = {} |
|
549 with self.security_enabled(False, False): |
|
550 for rtype, eids in relations: |
|
551 if self.vreg.schema[rtype].inlined: |
|
552 for fromeid, toeid in eids: |
|
553 if fromeid not in edited_entities: |
|
554 entity = self.entity_from_eid(fromeid) |
|
555 edited = EditedEntity(entity) |
|
556 edited_entities[fromeid] = edited |
|
557 else: |
|
558 edited = edited_entities[fromeid] |
|
559 edited.edited_attribute(rtype, toeid) |
|
560 else: |
|
561 relations_dict[rtype] = eids |
|
562 self.repo.glob_add_relations(self, relations_dict) |
|
563 for edited in edited_entities.values(): |
|
564 self.repo.glob_update_entity(self, edited) |
|
565 |
|
566 |
|
567 @_open_only |
|
568 def delete_relation(self, fromeid, rtype, toeid): |
|
569 """provide direct access to the repository method to delete a relation. |
|
570 |
|
571 This is equivalent to the following rql query: |
|
572 |
|
573 DELETE X rtype Y WHERE X eid fromeid, T eid toeid |
|
574 |
|
575 without read security check but also all the burden of rql execution. |
|
576 You may use this in hooks when you know both eids of the relation you |
|
577 want to delete. |
|
578 """ |
|
579 with self.security_enabled(False, False): |
|
580 if self.vreg.schema[rtype].inlined: |
|
581 entity = self.entity_from_eid(fromeid) |
|
582 entity.cw_attr_cache[rtype] = None |
|
583 self.repo.glob_update_entity(self, entity, set((rtype,))) |
|
584 else: |
|
585 self.repo.glob_delete_relation(self, fromeid, rtype, toeid) |
|
586 |
|
587 # relations cache handling ################################################# |
|
588 |
|
589 @_open_only |
|
590 def update_rel_cache_add(self, subject, rtype, object, symmetric=False): |
|
591 self._update_entity_rel_cache_add(subject, rtype, 'subject', object) |
|
592 if symmetric: |
|
593 self._update_entity_rel_cache_add(object, rtype, 'subject', subject) |
|
594 else: |
|
595 self._update_entity_rel_cache_add(object, rtype, 'object', subject) |
|
596 |
|
597 @_open_only |
|
598 def update_rel_cache_del(self, subject, rtype, object, symmetric=False): |
|
599 self._update_entity_rel_cache_del(subject, rtype, 'subject', object) |
|
600 if symmetric: |
|
601 self._update_entity_rel_cache_del(object, rtype, 'object', object) |
|
602 else: |
|
603 self._update_entity_rel_cache_del(object, rtype, 'object', subject) |
|
604 |
|
605 @_open_only |
|
606 def _update_entity_rel_cache_add(self, eid, rtype, role, targeteid): |
|
607 try: |
|
608 entity = self.entity_cache(eid) |
|
609 except KeyError: |
|
610 return |
|
611 rcache = entity.cw_relation_cached(rtype, role) |
|
612 if rcache is not None: |
|
613 rset, entities = rcache |
|
614 rset = rset.copy() |
|
615 entities = list(entities) |
|
616 rset.rows.append([targeteid]) |
|
617 if not isinstance(rset.description, list): # else description not set |
|
618 rset.description = list(rset.description) |
|
619 rset.description.append([self.entity_metas(targeteid)['type']]) |
|
620 targetentity = self.entity_from_eid(targeteid) |
|
621 if targetentity.cw_rset is None: |
|
622 targetentity.cw_rset = rset |
|
623 targetentity.cw_row = rset.rowcount |
|
624 targetentity.cw_col = 0 |
|
625 rset.rowcount += 1 |
|
626 entities.append(targetentity) |
|
627 entity._cw_related_cache['%s_%s' % (rtype, role)] = ( |
|
628 rset, tuple(entities)) |
|
629 |
|
630 @_open_only |
|
631 def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid): |
|
632 try: |
|
633 entity = self.entity_cache(eid) |
|
634 except KeyError: |
|
635 return |
|
636 rcache = entity.cw_relation_cached(rtype, role) |
|
637 if rcache is not None: |
|
638 rset, entities = rcache |
|
639 for idx, row in enumerate(rset.rows): |
|
640 if row[0] == targeteid: |
|
641 break |
|
642 else: |
|
643 # this may occurs if the cache has been filed by a hook |
|
644 # after the database update |
|
645 self.debug('cache inconsistency for %s %s %s %s', eid, rtype, |
|
646 role, targeteid) |
|
647 return |
|
648 rset = rset.copy() |
|
649 entities = list(entities) |
|
650 del rset.rows[idx] |
|
651 if isinstance(rset.description, list): # else description not set |
|
652 del rset.description[idx] |
|
653 del entities[idx] |
|
654 rset.rowcount -= 1 |
|
655 entity._cw_related_cache['%s_%s' % (rtype, role)] = ( |
|
656 rset, tuple(entities)) |
|
657 |
|
658 # Tracking of entities added of removed in the transaction ################## |
|
659 |
|
660 @_open_only |
|
661 def deleted_in_transaction(self, eid): |
|
662 """return True if the entity of the given eid is being deleted in the |
|
663 current transaction |
|
664 """ |
|
665 return eid in self.transaction_data.get('pendingeids', ()) |
|
666 |
|
667 @_open_only |
|
668 def added_in_transaction(self, eid): |
|
669 """return True if the entity of the given eid is being created in the |
|
670 current transaction |
|
671 """ |
|
672 return eid in self.transaction_data.get('neweids', ()) |
|
673 |
|
674 # Operation management #################################################### |
|
675 |
|
676 @_open_only |
|
677 def add_operation(self, operation, index=None): |
|
678 """add an operation to be executed at the end of the transaction""" |
|
679 if index is None: |
|
680 self.pending_operations.append(operation) |
|
681 else: |
|
682 self.pending_operations.insert(index, operation) |
|
683 |
|
684 # Hooks control ########################################################### |
|
685 |
|
686 @_open_only |
|
687 def allow_all_hooks_but(self, *categories): |
|
688 return _hooks_control(self, HOOKS_ALLOW_ALL, *categories) |
|
689 |
|
690 @_open_only |
|
691 def deny_all_hooks_but(self, *categories): |
|
692 return _hooks_control(self, HOOKS_DENY_ALL, *categories) |
|
693 |
|
694 @_open_only |
|
695 def disable_hook_categories(self, *categories): |
|
696 """disable the given hook categories: |
|
697 |
|
698 - on HOOKS_DENY_ALL mode, ensure those categories are not enabled |
|
699 - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled |
|
700 """ |
|
701 changes = set() |
|
702 self.pruned_hooks_cache.clear() |
|
703 categories = set(categories) |
|
704 if self.hooks_mode is HOOKS_DENY_ALL: |
|
705 enabledcats = self.enabled_hook_cats |
|
706 changes = enabledcats & categories |
|
707 enabledcats -= changes # changes is small hence faster |
|
708 else: |
|
709 disabledcats = self.disabled_hook_cats |
|
710 changes = categories - disabledcats |
|
711 disabledcats |= changes # changes is small hence faster |
|
712 return tuple(changes) |
|
713 |
|
714 @_open_only |
|
715 def enable_hook_categories(self, *categories): |
|
716 """enable the given hook categories: |
|
717 |
|
718 - on HOOKS_DENY_ALL mode, ensure those categories are enabled |
|
719 - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled |
|
720 """ |
|
721 changes = set() |
|
722 self.pruned_hooks_cache.clear() |
|
723 categories = set(categories) |
|
724 if self.hooks_mode is HOOKS_DENY_ALL: |
|
725 enabledcats = self.enabled_hook_cats |
|
726 changes = categories - enabledcats |
|
727 enabledcats |= changes # changes is small hence faster |
|
728 else: |
|
729 disabledcats = self.disabled_hook_cats |
|
730 changes = disabledcats & categories |
|
731 disabledcats -= changes # changes is small hence faster |
|
732 return tuple(changes) |
|
733 |
|
734 @_open_only |
|
735 def is_hook_category_activated(self, category): |
|
736 """return a boolean telling if the given category is currently activated |
|
737 or not |
|
738 """ |
|
739 if self.hooks_mode is HOOKS_DENY_ALL: |
|
740 return category in self.enabled_hook_cats |
|
741 return category not in self.disabled_hook_cats |
|
742 |
|
743 @_open_only |
|
744 def is_hook_activated(self, hook): |
|
745 """return a boolean telling if the given hook class is currently |
|
746 activated or not |
|
747 """ |
|
748 return self.is_hook_category_activated(hook.category) |
|
749 |
|
750 # Security management ##################################################### |
|
751 |
|
752 @_open_only |
|
753 def security_enabled(self, read=None, write=None): |
|
754 return _security_enabled(self, read=read, write=write) |
|
755 |
|
756 @property |
|
757 @_open_only |
|
758 def read_security(self): |
|
759 return self._read_security |
|
760 |
|
761 @read_security.setter |
|
762 @_open_only |
|
763 def read_security(self, activated): |
|
764 self._read_security = activated |
|
765 |
|
766 # undo support ############################################################ |
|
767 |
|
768 @_open_only |
|
769 def ertype_supports_undo(self, ertype): |
|
770 return self.undo_actions and ertype not in NO_UNDO_TYPES |
|
771 |
|
772 @_open_only |
|
773 def transaction_uuid(self, set=True): |
|
774 uuid = self.transaction_data.get('tx_uuid') |
|
775 if set and uuid is None: |
|
776 self.transaction_data['tx_uuid'] = uuid = text_type(uuid4().hex) |
|
777 self.repo.system_source.start_undoable_transaction(self, uuid) |
|
778 return uuid |
|
779 |
|
780 @_open_only |
|
781 def transaction_inc_action_counter(self): |
|
782 num = self.transaction_data.setdefault('tx_action_count', 0) + 1 |
|
783 self.transaction_data['tx_action_count'] = num |
|
784 return num |
|
785 |
|
786 # db-api like interface ################################################### |
|
787 |
|
788 @_open_only |
|
789 def source_defs(self): |
|
790 return self.repo.source_defs() |
|
791 |
|
792 @deprecated('[3.19] use .entity_metas(eid) instead') |
|
793 @_open_only |
|
794 def describe(self, eid, asdict=False): |
|
795 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
796 etype, extid, source = self.repo.type_and_source_from_eid(eid, self) |
|
797 metas = {'type': etype, 'source': source, 'extid': extid} |
|
798 if asdict: |
|
799 metas['asource'] = metas['source'] # XXX pre 3.19 client compat |
|
800 return metas |
|
801 return etype, source, extid |
|
802 |
|
803 @_open_only |
|
804 def entity_metas(self, eid): |
|
805 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
806 etype, extid, source = self.repo.type_and_source_from_eid(eid, self) |
|
807 return {'type': etype, 'source': source, 'extid': extid} |
|
808 |
|
809 # core method ############################################################# |
|
810 |
|
811 @_open_only |
|
812 def execute(self, rql, kwargs=None, build_descr=True): |
|
813 """db-api like method directly linked to the querier execute method. |
|
814 |
|
815 See :meth:`cubicweb.dbapi.Cursor.execute` documentation. |
|
816 """ |
|
817 self._session_timestamp.touch() |
|
818 rset = self._execute(self, rql, kwargs, build_descr) |
|
819 rset.req = self |
|
820 self._session_timestamp.touch() |
|
821 return rset |
|
822 |
|
823 @_open_only |
|
824 def rollback(self, free_cnxset=None, reset_pool=None): |
|
825 """rollback the current transaction""" |
|
826 if free_cnxset is not None: |
|
827 warn('[3.21] free_cnxset is now unneeded', |
|
828 DeprecationWarning, stacklevel=2) |
|
829 if reset_pool is not None: |
|
830 warn('[3.13] reset_pool is now unneeded', |
|
831 DeprecationWarning, stacklevel=2) |
|
832 cnxset = self.cnxset |
|
833 assert cnxset is not None |
|
834 try: |
|
835 # by default, operations are executed with security turned off |
|
836 with self.security_enabled(False, False): |
|
837 while self.pending_operations: |
|
838 try: |
|
839 operation = self.pending_operations.pop(0) |
|
840 operation.handle_event('rollback_event') |
|
841 except BaseException: |
|
842 self.critical('rollback error', exc_info=sys.exc_info()) |
|
843 continue |
|
844 cnxset.rollback() |
|
845 self.debug('rollback for transaction %s done', self.connectionid) |
|
846 finally: |
|
847 self._session_timestamp.touch() |
|
848 self.clear() |
|
849 |
|
850 @_open_only |
|
851 def commit(self, free_cnxset=None, reset_pool=None): |
|
852 """commit the current session's transaction""" |
|
853 if free_cnxset is not None: |
|
854 warn('[3.21] free_cnxset is now unneeded', |
|
855 DeprecationWarning, stacklevel=2) |
|
856 if reset_pool is not None: |
|
857 warn('[3.13] reset_pool is now unneeded', |
|
858 DeprecationWarning, stacklevel=2) |
|
859 assert self.cnxset is not None |
|
860 cstate = self.commit_state |
|
861 if cstate == 'uncommitable': |
|
862 raise QueryError('transaction must be rolled back') |
|
863 if cstate == 'precommit': |
|
864 self.warn('calling commit in precommit makes no sense; ignoring commit') |
|
865 return |
|
866 if cstate == 'postcommit': |
|
867 self.critical('postcommit phase is not allowed to write to the db; ignoring commit') |
|
868 return |
|
869 assert cstate is None |
|
870 # on rollback, an operation should have the following state |
|
871 # information: |
|
872 # - processed by the precommit/commit event or not |
|
873 # - if processed, is it the failed operation |
|
874 debug = server.DEBUG & server.DBG_OPS |
|
875 try: |
|
876 # by default, operations are executed with security turned off |
|
877 with self.security_enabled(False, False): |
|
878 processed = [] |
|
879 self.commit_state = 'precommit' |
|
880 if debug: |
|
881 print(self.commit_state, '*' * 20) |
|
882 try: |
|
883 with self.running_hooks_ops(): |
|
884 while self.pending_operations: |
|
885 operation = self.pending_operations.pop(0) |
|
886 operation.processed = 'precommit' |
|
887 processed.append(operation) |
|
888 if debug: |
|
889 print(operation) |
|
890 operation.handle_event('precommit_event') |
|
891 self.pending_operations[:] = processed |
|
892 self.debug('precommit transaction %s done', self.connectionid) |
|
893 except BaseException: |
|
894 # if error on [pre]commit: |
|
895 # |
|
896 # * set .failed = True on the operation causing the failure |
|
897 # * call revert<event>_event on processed operations |
|
898 # * call rollback_event on *all* operations |
|
899 # |
|
900 # that seems more natural than not calling rollback_event |
|
901 # for processed operations, and allow generic rollback |
|
902 # instead of having to implements rollback, revertprecommit |
|
903 # and revertcommit, that will be enough in mont case. |
|
904 operation.failed = True |
|
905 if debug: |
|
906 print(self.commit_state, '*' * 20) |
|
907 with self.running_hooks_ops(): |
|
908 for operation in reversed(processed): |
|
909 if debug: |
|
910 print(operation) |
|
911 try: |
|
912 operation.handle_event('revertprecommit_event') |
|
913 except BaseException: |
|
914 self.critical('error while reverting precommit', |
|
915 exc_info=True) |
|
916 # XXX use slice notation since self.pending_operations is a |
|
917 # read-only property. |
|
918 self.pending_operations[:] = processed + self.pending_operations |
|
919 self.rollback() |
|
920 raise |
|
921 self.cnxset.commit() |
|
922 self.commit_state = 'postcommit' |
|
923 if debug: |
|
924 print(self.commit_state, '*' * 20) |
|
925 with self.running_hooks_ops(): |
|
926 while self.pending_operations: |
|
927 operation = self.pending_operations.pop(0) |
|
928 if debug: |
|
929 print(operation) |
|
930 operation.processed = 'postcommit' |
|
931 try: |
|
932 operation.handle_event('postcommit_event') |
|
933 except BaseException: |
|
934 self.critical('error while postcommit', |
|
935 exc_info=sys.exc_info()) |
|
936 self.debug('postcommit transaction %s done', self.connectionid) |
|
937 return self.transaction_uuid(set=False) |
|
938 finally: |
|
939 self._session_timestamp.touch() |
|
940 self.clear() |
|
941 |
|
942 # resource accessors ###################################################### |
|
943 |
|
944 @_open_only |
|
945 def call_service(self, regid, **kwargs): |
|
946 self.debug('calling service %s', regid) |
|
947 service = self.vreg['services'].select(regid, self, **kwargs) |
|
948 return service.call(**kwargs) |
|
949 |
|
950 @_open_only |
|
951 def system_sql(self, sql, args=None, rollback_on_failure=True): |
|
952 """return a sql cursor on the system database""" |
|
953 source = self.repo.system_source |
|
954 try: |
|
955 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
956 except (source.OperationalError, source.InterfaceError): |
|
957 if not rollback_on_failure: |
|
958 raise |
|
959 source.warning("trying to reconnect") |
|
960 self.cnxset.reconnect() |
|
961 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
962 |
|
963 @_open_only |
|
964 def rtype_eids_rdef(self, rtype, eidfrom, eidto): |
|
965 # use type_and_source_from_eid instead of type_from_eid for optimization |
|
966 # (avoid two extra methods call) |
|
967 subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0] |
|
968 objtype = self.repo.type_and_source_from_eid(eidto, self)[0] |
|
969 return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)] |
|
970 |
|
971 |
|
972 def cnx_attr(attr_name, writable=False): |
|
973 """return a property to forward attribute access to connection. |
|
974 |
|
975 This is to be used by session""" |
|
976 args = {} |
|
977 @deprecated('[3.19] use a Connection object instead') |
|
978 def attr_from_cnx(session): |
|
979 return getattr(session._cnx, attr_name) |
|
980 args['fget'] = attr_from_cnx |
|
981 if writable: |
|
982 @deprecated('[3.19] use a Connection object instead') |
|
983 def write_attr(session, value): |
|
984 return setattr(session._cnx, attr_name, value) |
|
985 args['fset'] = write_attr |
|
986 return property(**args) |
|
987 |
|
988 |
|
989 class Timestamp(object): |
|
990 |
|
991 def __init__(self): |
|
992 self.value = time() |
|
993 |
|
994 def touch(self): |
|
995 self.value = time() |
|
996 |
|
997 def __float__(self): |
|
998 return float(self.value) |
|
999 |
|
1000 |
|
1001 class Session(object): |
|
1002 """Repository user session |
|
1003 |
|
1004 This ties all together: |
|
1005 * session id, |
|
1006 * user, |
|
1007 * other session data. |
|
1008 """ |
|
1009 |
|
1010 def __init__(self, user, repo, cnxprops=None, _id=None): |
|
1011 self.sessionid = _id or make_uid(unormalize(user.login)) |
|
1012 self.user = user # XXX repoapi: deprecated and store only a login. |
|
1013 self.repo = repo |
|
1014 self.vreg = repo.vreg |
|
1015 self._timestamp = Timestamp() |
|
1016 self.data = {} |
|
1017 self.closed = False |
|
1018 |
|
1019 def close(self): |
|
1020 self.closed = True |
|
1021 |
|
1022 def __enter__(self): |
|
1023 return self |
|
1024 |
|
1025 def __exit__(self, *args): |
|
1026 pass |
|
1027 |
|
1028 def __unicode__(self): |
|
1029 return '<session %s (%s 0x%x)>' % ( |
|
1030 unicode(self.user.login), self.sessionid, id(self)) |
|
1031 |
|
1032 @property |
|
1033 def timestamp(self): |
|
1034 return float(self._timestamp) |
|
1035 |
|
1036 @property |
|
1037 @deprecated('[3.19] session.id is deprecated, use session.sessionid') |
|
1038 def id(self): |
|
1039 return self.sessionid |
|
1040 |
|
1041 @property |
|
1042 def login(self): |
|
1043 return self.user.login |
|
1044 |
|
1045 def new_cnx(self): |
|
1046 """Return a new Connection object linked to the session |
|
1047 |
|
1048 The returned Connection will *not* be managed by the Session. |
|
1049 """ |
|
1050 return Connection(self) |
|
1051 |
|
1052 @deprecated('[3.19] use a Connection object instead') |
|
1053 def get_option_value(self, option, foreid=None): |
|
1054 if foreid is not None: |
|
1055 warn('[3.19] foreid argument is deprecated', DeprecationWarning, |
|
1056 stacklevel=2) |
|
1057 return self.repo.get_option_value(option) |
|
1058 |
|
1059 def _touch(self): |
|
1060 """update latest session usage timestamp and reset mode to read""" |
|
1061 self._timestamp.touch() |
|
1062 |
|
1063 local_perm_cache = cnx_attr('local_perm_cache') |
|
1064 @local_perm_cache.setter |
|
1065 def local_perm_cache(self, value): |
|
1066 #base class assign an empty dict:-( |
|
1067 assert value == {} |
|
1068 pass |
|
1069 |
|
1070 # deprecated ############################################################### |
|
1071 |
|
1072 @property |
|
1073 def anonymous_session(self): |
|
1074 # XXX for now, anonymous_user only exists in webconfig (and testconfig). |
|
1075 # It will only be present inside all-in-one instance. |
|
1076 # there is plan to move it down to global config. |
|
1077 if not hasattr(self.repo.config, 'anonymous_user'): |
|
1078 # not a web or test config, no anonymous user |
|
1079 return False |
|
1080 return self.user.login == self.repo.config.anonymous_user()[0] |
|
1081 |
|
1082 @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)') |
|
1083 def schema_rproperty(self, rtype, eidfrom, eidto, rprop): |
|
1084 return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop) |
|
1085 |
|
1086 # these are overridden by set_log_methods below |
|
1087 # only defining here to prevent pylint from complaining |
|
1088 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
|
1089 |
|
1090 |
|
1091 |
|
1092 class InternalManager(object): |
|
1093 """a manager user with all access rights used internally for task such as |
|
1094 bootstrapping the repository or creating regular users according to |
|
1095 repository content |
|
1096 """ |
|
1097 |
|
1098 def __init__(self, lang='en'): |
|
1099 self.eid = -1 |
|
1100 self.login = u'__internal_manager__' |
|
1101 self.properties = {} |
|
1102 self.groups = set(['managers']) |
|
1103 self.lang = lang |
|
1104 |
|
1105 def matching_groups(self, groups): |
|
1106 return 1 |
|
1107 |
|
1108 def is_in_group(self, group): |
|
1109 return True |
|
1110 |
|
1111 def owns(self, eid): |
|
1112 return True |
|
1113 |
|
1114 def property_value(self, key): |
|
1115 if key == 'ui.language': |
|
1116 return self.lang |
|
1117 return None |
|
1118 |
|
1119 def prefered_language(self, language=None): |
|
1120 # mock CWUser.prefered_language, mainly for testing purpose |
|
1121 return self.property_value('ui.language') |
|
1122 |
|
1123 # CWUser compat for notification ########################################### |
|
1124 |
|
1125 def name(self): |
|
1126 return 'cubicweb' |
|
1127 |
|
1128 class _IEmailable: |
|
1129 @staticmethod |
|
1130 def get_email(): |
|
1131 return '' |
|
1132 |
|
1133 def cw_adapt_to(self, iface): |
|
1134 if iface == 'IEmailable': |
|
1135 return self._IEmailable |
|
1136 return None |
|
1137 |
|
1138 from logging import getLogger |
|
1139 from cubicweb import set_log_methods |
|
1140 set_log_methods(Session, getLogger('cubicweb.session')) |
|
1141 set_log_methods(Connection, getLogger('cubicweb.session')) |
|