18 """Repository users' and internal' sessions.""" |
18 """Repository users' and internal' sessions.""" |
19 from __future__ import print_function |
19 from __future__ import print_function |
20 |
20 |
21 __docformat__ = "restructuredtext en" |
21 __docformat__ = "restructuredtext en" |
22 |
22 |
|
23 import functools |
23 import sys |
24 import sys |
24 from time import time |
25 from time import time |
25 from uuid import uuid4 |
26 from uuid import uuid4 |
26 from warnings import warn |
27 from warnings import warn |
27 import functools |
|
28 from contextlib import contextmanager |
28 from contextlib import contextmanager |
|
29 from logging import getLogger |
29 |
30 |
30 from six import text_type |
31 from six import text_type |
31 |
32 |
32 from logilab.common.deprecation import deprecated |
33 from logilab.common.deprecation import deprecated |
33 from logilab.common.textutils import unormalize |
34 from logilab.common.textutils import unormalize |
34 from logilab.common.registry import objectify_predicate |
35 from logilab.common.registry import objectify_predicate |
35 |
36 |
36 from cubicweb import QueryError, schema, server, ProgrammingError |
37 from cubicweb import QueryError, ProgrammingError, schema, server |
|
38 from cubicweb import set_log_methods |
37 from cubicweb.req import RequestSessionBase |
39 from cubicweb.req import RequestSessionBase |
38 from cubicweb.utils import make_uid |
40 from cubicweb.utils import make_uid |
39 from cubicweb.rqlrewrite import RQLRewriter |
41 from cubicweb.rqlrewrite import RQLRewriter |
40 from cubicweb.server.edition import EditedEntity |
42 from cubicweb.server.edition import EditedEntity |
41 |
43 |
48 NO_UNDO_TYPES.add('is') |
50 NO_UNDO_TYPES.add('is') |
49 NO_UNDO_TYPES.add('is_instance_of') |
51 NO_UNDO_TYPES.add('is_instance_of') |
50 NO_UNDO_TYPES.add('cw_source') |
52 NO_UNDO_TYPES.add('cw_source') |
51 # XXX rememberme,forgotpwd,apycot,vcsfile |
53 # XXX rememberme,forgotpwd,apycot,vcsfile |
52 |
54 |
|
55 |
53 @objectify_predicate |
56 @objectify_predicate |
54 def is_user_session(cls, req, **kwargs): |
57 def is_user_session(cls, req, **kwargs): |
55 """return 1 when session is not internal. |
58 """return 1 when session is not internal. |
56 |
59 |
57 This predicate can only be used repository side only. """ |
60 This predicate can only be used repository side only. """ |
58 return not req.is_internal_session |
61 return not req.is_internal_session |
59 |
62 |
|
63 |
60 @objectify_predicate |
64 @objectify_predicate |
61 def is_internal_session(cls, req, **kwargs): |
65 def is_internal_session(cls, req, **kwargs): |
62 """return 1 when session is not internal. |
66 """return 1 when session is not internal. |
63 |
67 |
64 This predicate can only be used repository side only. """ |
68 This predicate can only be used repository side only. """ |
65 return req.is_internal_session |
69 return req.is_internal_session |
|
70 |
66 |
71 |
67 @objectify_predicate |
72 @objectify_predicate |
68 def repairing(cls, req, **kwargs): |
73 def repairing(cls, req, **kwargs): |
69 """return 1 when repository is running in repair mode""" |
74 """return 1 when repository is running in repair mode""" |
70 return req.vreg.config.repairing |
75 return req.vreg.config.repairing |
71 |
76 |
72 |
77 |
73 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead') |
78 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead') |
74 def hooks_control(obj, mode, *categories): |
79 def hooks_control(obj, mode, *categories): |
75 assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL) |
80 assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL) |
76 if mode == HOOKS_ALLOW_ALL: |
81 if mode == HOOKS_ALLOW_ALL: |
77 return obj.allow_all_hooks_but(*categories) |
82 return obj.allow_all_hooks_but(*categories) |
78 elif mode == HOOKS_DENY_ALL: |
83 elif mode == HOOKS_DENY_ALL: |
79 return obj.deny_all_hooks_but(*categories) |
84 return obj.deny_all_hooks_but(*categories) |
80 |
85 |
130 |
135 |
131 @deprecated('[3.17] use <object>.security_enabled instead') |
136 @deprecated('[3.17] use <object>.security_enabled instead') |
132 def security_enabled(obj, *args, **kwargs): |
137 def security_enabled(obj, *args, **kwargs): |
133 return obj.security_enabled(*args, **kwargs) |
138 return obj.security_enabled(*args, **kwargs) |
134 |
139 |
|
140 |
135 class _security_enabled(object): |
141 class _security_enabled(object): |
136 """context manager to control security w/ session.execute, |
142 """context manager to control security w/ session.execute, |
137 |
143 |
138 By default security is disabled on queries executed on the repository |
144 By default security is disabled on queries executed on the repository |
139 side. |
145 side. |
163 if self.oldwrite is not None: |
169 if self.oldwrite is not None: |
164 self.cnx.write_security = self.oldwrite |
170 self.cnx.write_security = self.oldwrite |
165 |
171 |
166 HOOKS_ALLOW_ALL = object() |
172 HOOKS_ALLOW_ALL = object() |
167 HOOKS_DENY_ALL = object() |
173 HOOKS_DENY_ALL = object() |
168 DEFAULT_SECURITY = object() # evaluated to true by design |
174 DEFAULT_SECURITY = object() # evaluated to true by design |
|
175 |
169 |
176 |
170 class SessionClosedError(RuntimeError): |
177 class SessionClosedError(RuntimeError): |
171 pass |
178 pass |
172 |
179 |
173 |
180 |
175 """decorator for Connection method that check it is open""" |
182 """decorator for Connection method that check it is open""" |
176 @functools.wraps(func) |
183 @functools.wraps(func) |
177 def check_open(cnx, *args, **kwargs): |
184 def check_open(cnx, *args, **kwargs): |
178 if not cnx._open: |
185 if not cnx._open: |
179 raise ProgrammingError('Closed Connection: %s' |
186 raise ProgrammingError('Closed Connection: %s' |
180 % cnx.connectionid) |
187 % cnx.connectionid) |
181 return func(cnx, *args, **kwargs) |
188 return func(cnx, *args, **kwargs) |
182 return check_open |
189 return check_open |
183 |
190 |
184 |
191 |
185 class Connection(RequestSessionBase): |
192 class Connection(RequestSessionBase): |
273 #: ordered list of operations to be processed on commit/rollback |
280 #: ordered list of operations to be processed on commit/rollback |
274 self.pending_operations = [] |
281 self.pending_operations = [] |
275 #: (None, 'precommit', 'postcommit', 'uncommitable') |
282 #: (None, 'precommit', 'postcommit', 'uncommitable') |
276 self.commit_state = None |
283 self.commit_state = None |
277 |
284 |
278 ### hook control attribute |
285 # hook control attribute |
279 self.hooks_mode = HOOKS_ALLOW_ALL |
286 self.hooks_mode = HOOKS_ALLOW_ALL |
280 self.disabled_hook_cats = set() |
287 self.disabled_hook_cats = set() |
281 self.enabled_hook_cats = set() |
288 self.enabled_hook_cats = set() |
282 self.pruned_hooks_cache = {} |
289 self.pruned_hooks_cache = {} |
283 |
290 |
284 |
291 # security control attributes |
285 ### security control attributes |
292 self._read_security = DEFAULT_SECURITY # handled by a property |
286 self._read_security = DEFAULT_SECURITY # handled by a property |
|
287 self.write_security = DEFAULT_SECURITY |
293 self.write_security = DEFAULT_SECURITY |
288 |
294 |
289 # undo control |
295 # undo control |
290 config = session.repo.config |
296 config = session.repo.config |
291 if config.creating or config.repairing or self.is_internal_session: |
297 if config.creating or config.repairing or self.is_internal_session: |
302 self.set_language(self.user.prefered_language()) |
308 self.set_language(self.user.prefered_language()) |
303 else: |
309 else: |
304 self._set_user(session.user) |
310 self._set_user(session.user) |
305 |
311 |
306 @_open_only |
312 @_open_only |
307 def source_defs(self): |
|
308 """Return the definition of sources used by the repository.""" |
|
309 return self.session.repo.source_defs() |
|
310 |
|
311 @_open_only |
|
312 def get_schema(self): |
313 def get_schema(self): |
313 """Return the schema currently used by the repository.""" |
314 """Return the schema currently used by the repository.""" |
314 return self.session.repo.source_defs() |
315 return self.session.repo.source_defs() |
315 |
316 |
316 @_open_only |
317 @_open_only |
379 return self.repo.system_source.undo_transaction(self, txuuid) |
380 return self.repo.system_source.undo_transaction(self, txuuid) |
380 |
381 |
381 # life cycle handling #################################################### |
382 # life cycle handling #################################################### |
382 |
383 |
383 def __enter__(self): |
384 def __enter__(self): |
384 assert self._open is None # first opening |
385 assert self._open is None # first opening |
385 self._open = True |
386 self._open = True |
386 self.cnxset = self.repo._get_cnxset() |
387 self.cnxset = self.repo._get_cnxset() |
387 return self |
388 return self |
388 |
389 |
389 def __exit__(self, exctype=None, excvalue=None, tb=None): |
390 def __exit__(self, exctype=None, excvalue=None, tb=None): |
390 assert self._open # actually already open |
391 assert self._open # actually already open |
391 self.rollback() |
392 self.rollback() |
392 self._open = False |
393 self._open = False |
393 self.cnxset.cnxset_freed() |
394 self.cnxset.cnxset_freed() |
394 self.repo._free_cnxset(self.cnxset) |
395 self.repo._free_cnxset(self.cnxset) |
395 self.cnxset = None |
396 self.cnxset = None |
485 def set_entity_cache(self, entity): |
486 def set_entity_cache(self, entity): |
486 """Add `entity` to the connection entity cache""" |
487 """Add `entity` to the connection entity cache""" |
487 # XXX not using _open_only because before at creation time. _set_user |
488 # XXX not using _open_only because before at creation time. _set_user |
488 # call this function to cache the Connection user. |
489 # call this function to cache the Connection user. |
489 if entity.cw_etype != 'CWUser' and not self._open: |
490 if entity.cw_etype != 'CWUser' and not self._open: |
490 raise ProgrammingError('Closed Connection: %s' |
491 raise ProgrammingError('Closed Connection: %s' % self.connectionid) |
491 % self.connectionid) |
|
492 ecache = self.transaction_data.setdefault('ecache', {}) |
492 ecache = self.transaction_data.setdefault('ecache', {}) |
493 ecache.setdefault(entity.eid, entity) |
493 ecache.setdefault(entity.eid, entity) |
494 |
494 |
495 @_open_only |
495 @_open_only |
496 def entity_cache(self, eid): |
496 def entity_cache(self, eid): |
524 |
524 |
525 without read security check but also all the burden of rql execution. |
525 without read security check but also all the burden of rql execution. |
526 You may use this in hooks when you know both eids of the relation you |
526 You may use this in hooks when you know both eids of the relation you |
527 want to add. |
527 want to add. |
528 """ |
528 """ |
529 self.add_relations([(rtype, [(fromeid, toeid)])]) |
529 self.add_relations([(rtype, [(fromeid, toeid)])]) |
530 |
530 |
531 @_open_only |
531 @_open_only |
532 def add_relations(self, relations): |
532 def add_relations(self, relations): |
533 '''set many relation using a shortcut similar to the one in add_relation |
533 '''set many relation using a shortcut similar to the one in add_relation |
534 |
534 |
553 relations_dict[rtype] = eids |
553 relations_dict[rtype] = eids |
554 self.repo.glob_add_relations(self, relations_dict) |
554 self.repo.glob_add_relations(self, relations_dict) |
555 for edited in edited_entities.values(): |
555 for edited in edited_entities.values(): |
556 self.repo.glob_update_entity(self, edited) |
556 self.repo.glob_update_entity(self, edited) |
557 |
557 |
558 |
|
559 @_open_only |
558 @_open_only |
560 def delete_relation(self, fromeid, rtype, toeid): |
559 def delete_relation(self, fromeid, rtype, toeid): |
561 """provide direct access to the repository method to delete a relation. |
560 """provide direct access to the repository method to delete a relation. |
562 |
561 |
563 This is equivalent to the following rql query: |
562 This is equivalent to the following rql query: |
604 if rcache is not None: |
603 if rcache is not None: |
605 rset, entities = rcache |
604 rset, entities = rcache |
606 rset = rset.copy() |
605 rset = rset.copy() |
607 entities = list(entities) |
606 entities = list(entities) |
608 rset.rows.append([targeteid]) |
607 rset.rows.append([targeteid]) |
609 if not isinstance(rset.description, list): # else description not set |
608 if not isinstance(rset.description, list): # else description not set |
610 rset.description = list(rset.description) |
609 rset.description = list(rset.description) |
611 rset.description.append([self.entity_metas(targeteid)['type']]) |
610 rset.description.append([self.entity_metas(targeteid)['type']]) |
612 targetentity = self.entity_from_eid(targeteid) |
611 targetentity = self.entity_from_eid(targeteid) |
613 if targetentity.cw_rset is None: |
612 if targetentity.cw_rset is None: |
614 targetentity.cw_rset = rset |
613 targetentity.cw_rset = rset |
638 role, targeteid) |
637 role, targeteid) |
639 return |
638 return |
640 rset = rset.copy() |
639 rset = rset.copy() |
641 entities = list(entities) |
640 entities = list(entities) |
642 del rset.rows[idx] |
641 del rset.rows[idx] |
643 if isinstance(rset.description, list): # else description not set |
642 if isinstance(rset.description, list): # else description not set |
644 del rset.description[idx] |
643 del rset.description[idx] |
645 del entities[idx] |
644 del entities[idx] |
646 rset.rowcount -= 1 |
645 rset.rowcount -= 1 |
647 entity._cw_related_cache['%s_%s' % (rtype, role)] = ( |
646 entity._cw_related_cache['%s_%s' % (rtype, role)] = ( |
648 rset, tuple(entities)) |
647 rset, tuple(entities)) |
694 self.pruned_hooks_cache.clear() |
693 self.pruned_hooks_cache.clear() |
695 categories = set(categories) |
694 categories = set(categories) |
696 if self.hooks_mode is HOOKS_DENY_ALL: |
695 if self.hooks_mode is HOOKS_DENY_ALL: |
697 enabledcats = self.enabled_hook_cats |
696 enabledcats = self.enabled_hook_cats |
698 changes = enabledcats & categories |
697 changes = enabledcats & categories |
699 enabledcats -= changes # changes is small hence faster |
698 enabledcats -= changes # changes is small hence faster |
700 else: |
699 else: |
701 disabledcats = self.disabled_hook_cats |
700 disabledcats = self.disabled_hook_cats |
702 changes = categories - disabledcats |
701 changes = categories - disabledcats |
703 disabledcats |= changes # changes is small hence faster |
702 disabledcats |= changes # changes is small hence faster |
704 return tuple(changes) |
703 return tuple(changes) |
705 |
704 |
706 @_open_only |
705 @_open_only |
707 def enable_hook_categories(self, *categories): |
706 def enable_hook_categories(self, *categories): |
708 """enable the given hook categories: |
707 """enable the given hook categories: |
714 self.pruned_hooks_cache.clear() |
713 self.pruned_hooks_cache.clear() |
715 categories = set(categories) |
714 categories = set(categories) |
716 if self.hooks_mode is HOOKS_DENY_ALL: |
715 if self.hooks_mode is HOOKS_DENY_ALL: |
717 enabledcats = self.enabled_hook_cats |
716 enabledcats = self.enabled_hook_cats |
718 changes = categories - enabledcats |
717 changes = categories - enabledcats |
719 enabledcats |= changes # changes is small hence faster |
718 enabledcats |= changes # changes is small hence faster |
720 else: |
719 else: |
721 disabledcats = self.disabled_hook_cats |
720 disabledcats = self.disabled_hook_cats |
722 changes = disabledcats & categories |
721 changes = disabledcats & categories |
723 disabledcats -= changes # changes is small hence faster |
722 disabledcats -= changes # changes is small hence faster |
724 return tuple(changes) |
723 return tuple(changes) |
725 |
724 |
726 @_open_only |
725 @_open_only |
727 def is_hook_category_activated(self, category): |
726 def is_hook_category_activated(self, category): |
728 """return a boolean telling if the given category is currently activated |
727 """return a boolean telling if the given category is currently activated |
786 def describe(self, eid, asdict=False): |
785 def describe(self, eid, asdict=False): |
787 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
786 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
788 etype, extid, source = self.repo.type_and_source_from_eid(eid, self) |
787 etype, extid, source = self.repo.type_and_source_from_eid(eid, self) |
789 metas = {'type': etype, 'source': source, 'extid': extid} |
788 metas = {'type': etype, 'source': source, 'extid': extid} |
790 if asdict: |
789 if asdict: |
791 metas['asource'] = metas['source'] # XXX pre 3.19 client compat |
790 metas['asource'] = metas['source'] # XXX pre 3.19 client compat |
792 return metas |
791 return metas |
793 return etype, source, extid |
792 return etype, source, extid |
794 |
793 |
795 @_open_only |
794 @_open_only |
796 def entity_metas(self, eid): |
795 def entity_metas(self, eid): |
964 def cnx_attr(attr_name, writable=False): |
963 def cnx_attr(attr_name, writable=False): |
965 """return a property to forward attribute access to connection. |
964 """return a property to forward attribute access to connection. |
966 |
965 |
967 This is to be used by session""" |
966 This is to be used by session""" |
968 args = {} |
967 args = {} |
|
968 |
969 @deprecated('[3.19] use a Connection object instead') |
969 @deprecated('[3.19] use a Connection object instead') |
970 def attr_from_cnx(session): |
970 def attr_from_cnx(session): |
971 return getattr(session._cnx, attr_name) |
971 return getattr(session._cnx, attr_name) |
|
972 |
972 args['fget'] = attr_from_cnx |
973 args['fget'] = attr_from_cnx |
973 if writable: |
974 if writable: |
974 @deprecated('[3.19] use a Connection object instead') |
975 @deprecated('[3.19] use a Connection object instead') |
975 def write_attr(session, value): |
976 def write_attr(session, value): |
976 return setattr(session._cnx, attr_name, value) |
977 return setattr(session._cnx, attr_name, value) |
999 * other session data. |
1000 * other session data. |
1000 """ |
1001 """ |
1001 |
1002 |
1002 def __init__(self, user, repo, _id=None): |
1003 def __init__(self, user, repo, _id=None): |
1003 self.sessionid = _id or make_uid(unormalize(user.login)) |
1004 self.sessionid = _id or make_uid(unormalize(user.login)) |
1004 self.user = user # XXX repoapi: deprecated and store only a login. |
1005 self.user = user # XXX repoapi: deprecated and store only a login. |
1005 self.repo = repo |
1006 self.repo = repo |
1006 self._timestamp = Timestamp() |
1007 self._timestamp = Timestamp() |
1007 self.data = {} |
1008 self.data = {} |
1008 self.closed = False |
1009 self.closed = False |
1009 |
1010 |
1052 def _touch(self): |
1053 def _touch(self): |
1053 """update latest session usage timestamp and reset mode to read""" |
1054 """update latest session usage timestamp and reset mode to read""" |
1054 self._timestamp.touch() |
1055 self._timestamp.touch() |
1055 |
1056 |
1056 local_perm_cache = cnx_attr('local_perm_cache') |
1057 local_perm_cache = cnx_attr('local_perm_cache') |
|
1058 |
1057 @local_perm_cache.setter |
1059 @local_perm_cache.setter |
1058 def local_perm_cache(self, value): |
1060 def local_perm_cache(self, value): |
1059 #base class assign an empty dict:-( |
1061 # base class assign an empty dict:-( |
1060 assert value == {} |
1062 assert value == {} |
1061 pass |
1063 pass |
1062 |
1064 |
1063 # deprecated ############################################################### |
1065 # deprecated ############################################################### |
1064 |
1066 |
1076 def schema_rproperty(self, rtype, eidfrom, eidto, rprop): |
1078 def schema_rproperty(self, rtype, eidfrom, eidto, rprop): |
1077 return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop) |
1079 return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop) |
1078 |
1080 |
1079 # these are overridden by set_log_methods below |
1081 # these are overridden by set_log_methods below |
1080 # only defining here to prevent pylint from complaining |
1082 # only defining here to prevent pylint from complaining |
1081 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
1083 info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None |
1082 |
|
1083 |
1084 |
1084 |
1085 |
1085 class InternalManager(object): |
1086 class InternalManager(object): |
1086 """a manager user with all access rights used internally for task such as |
1087 """a manager user with all access rights used internally for task such as |
1087 bootstrapping the repository or creating regular users according to |
1088 bootstrapping the repository or creating regular users according to |
1126 def cw_adapt_to(self, iface): |
1127 def cw_adapt_to(self, iface): |
1127 if iface == 'IEmailable': |
1128 if iface == 'IEmailable': |
1128 return self._IEmailable |
1129 return self._IEmailable |
1129 return None |
1130 return None |
1130 |
1131 |
1131 from logging import getLogger |
1132 |
1132 from cubicweb import set_log_methods |
|
1133 set_log_methods(Session, getLogger('cubicweb.session')) |
1133 set_log_methods(Session, getLogger('cubicweb.session')) |
1134 set_log_methods(Connection, getLogger('cubicweb.session')) |
1134 set_log_methods(Connection, getLogger('cubicweb.session')) |