69 self.session.disable_hooks_category(*self.changes) |
71 self.session.disable_hooks_category(*self.changes) |
70 else: |
72 else: |
71 self.session.enable_hooks_category(*self.changes) |
73 self.session.enable_hooks_category(*self.changes) |
72 self.session.set_hooks_mode(self.oldmode) |
74 self.session.set_hooks_mode(self.oldmode) |
73 |
75 |
|
76 INDENT = '' |
|
77 class security_enabled(object): |
|
78 """context manager to control security w/ session.execute, since by |
|
79 default security is disabled on queries executed on the repository |
|
80 side. |
|
81 """ |
|
82 def __init__(self, session, read=None, write=None): |
|
83 self.session = session |
|
84 self.read = read |
|
85 self.write = write |
|
86 |
|
87 def __enter__(self): |
|
88 # global INDENT |
|
89 if self.read is not None: |
|
90 self.oldread = self.session.set_read_security(self.read) |
|
91 # print INDENT + 'read', self.read, self.oldread |
|
92 if self.write is not None: |
|
93 self.oldwrite = self.session.set_write_security(self.write) |
|
94 # print INDENT + 'write', self.write, self.oldwrite |
|
95 # INDENT += ' ' |
|
96 |
|
97 def __exit__(self, exctype, exc, traceback): |
|
98 # global INDENT |
|
99 # INDENT = INDENT[:-2] |
|
100 if self.read is not None: |
|
101 self.session.set_read_security(self.oldread) |
|
102 # print INDENT + 'reset read to', self.oldread |
|
103 if self.write is not None: |
|
104 self.session.set_write_security(self.oldwrite) |
|
105 # print INDENT + 'reset write to', self.oldwrite |
|
106 |
74 |
107 |
75 |
108 |
76 class Session(RequestSessionBase): |
109 class Session(RequestSessionBase): |
77 """tie session id, user, connections pool and other session data all |
110 """tie session id, user, connections pool and other session data all |
78 together |
111 together |
107 self.cnxtype, unicode(self.user.login), self.id, id(self)) |
139 self.cnxtype, unicode(self.user.login), self.id, id(self)) |
108 |
140 |
109 def hijack_user(self, user): |
141 def hijack_user(self, user): |
110 """return a fake request/session using specified user""" |
142 """return a fake request/session using specified user""" |
111 session = Session(user, self.repo) |
143 session = Session(user, self.repo) |
112 session._threaddata = self.actual_session()._threaddata |
144 session._threaddata.pool = pool |
113 return session |
145 return session |
114 |
|
115 def _super_call(self, __cb, *args, **kwargs): |
|
116 if self.is_super_session: |
|
117 __cb(self, *args, **kwargs) |
|
118 return |
|
119 self.is_super_session = True |
|
120 try: |
|
121 __cb(self, *args, **kwargs) |
|
122 finally: |
|
123 self.is_super_session = False |
|
124 |
146 |
125 def add_relation(self, fromeid, rtype, toeid): |
147 def add_relation(self, fromeid, rtype, toeid): |
126 """provide direct access to the repository method to add a relation. |
148 """provide direct access to the repository method to add a relation. |
127 |
149 |
128 This is equivalent to the following rql query: |
150 This is equivalent to the following rql query: |
131 |
153 |
132 without read security check but also all the burden of rql execution. |
154 without read security check but also all the burden of rql execution. |
133 You may use this in hooks when you know both eids of the relation you |
155 You may use this in hooks when you know both eids of the relation you |
134 want to add. |
156 want to add. |
135 """ |
157 """ |
136 if self.vreg.schema[rtype].inlined: |
158 with security_enabled(self, False, False): |
137 entity = self.entity_from_eid(fromeid) |
159 if self.vreg.schema[rtype].inlined: |
138 entity[rtype] = toeid |
160 entity = self.entity_from_eid(fromeid) |
139 self._super_call(self.repo.glob_update_entity, |
161 entity[rtype] = toeid |
140 entity, set((rtype,))) |
162 self.repo.glob_update_entity(self, entity, set((rtype,))) |
141 else: |
163 else: |
142 self._super_call(self.repo.glob_add_relation, |
164 self.repo.glob_add_relation(self, fromeid, rtype, toeid) |
143 fromeid, rtype, toeid) |
|
144 |
165 |
145 def delete_relation(self, fromeid, rtype, toeid): |
166 def delete_relation(self, fromeid, rtype, toeid): |
146 """provide direct access to the repository method to delete a relation. |
167 """provide direct access to the repository method to delete a relation. |
147 |
168 |
148 This is equivalent to the following rql query: |
169 This is equivalent to the following rql query: |
151 |
172 |
152 without read security check but also all the burden of rql execution. |
173 without read security check but also all the burden of rql execution. |
153 You may use this in hooks when you know both eids of the relation you |
174 You may use this in hooks when you know both eids of the relation you |
154 want to delete. |
175 want to delete. |
155 """ |
176 """ |
156 if self.vreg.schema[rtype].inlined: |
177 with security_enabled(self, False, False): |
157 entity = self.entity_from_eid(fromeid) |
178 if self.vreg.schema[rtype].inlined: |
158 entity[rtype] = None |
179 entity = self.entity_from_eid(fromeid) |
159 self._super_call(self.repo.glob_update_entity, |
180 entity[rtype] = None |
160 entity, set((rtype,))) |
181 self.repo.glob_update_entity(self, entity, set((rtype,))) |
161 else: |
182 else: |
162 self._super_call(self.repo.glob_delete_relation, |
183 self.repo.glob_delete_relation(self, fromeid, rtype, toeid) |
163 fromeid, rtype, toeid) |
|
164 |
184 |
165 # relations cache handling ################################################# |
185 # relations cache handling ################################################# |
166 |
186 |
167 def update_rel_cache_add(self, subject, rtype, object, symmetric=False): |
187 def update_rel_cache_add(self, subject, rtype, object, symmetric=False): |
168 self._update_entity_rel_cache_add(subject, rtype, 'subject', object) |
188 self._update_entity_rel_cache_add(subject, rtype, 'subject', object) |
273 rschema = self.repo.schema[rtype] |
289 rschema = self.repo.schema[rtype] |
274 subjtype = self.describe(eidfrom)[0] |
290 subjtype = self.describe(eidfrom)[0] |
275 objtype = self.describe(eidto)[0] |
291 objtype = self.describe(eidto)[0] |
276 rdef = rschema.rdef(subjtype, objtype) |
292 rdef = rschema.rdef(subjtype, objtype) |
277 return rdef.get(rprop) |
293 return rdef.get(rprop) |
|
294 |
|
295 # security control ######################################################### |
|
296 |
|
297 DEFAULT_SECURITY = object() # evaluated to true by design |
|
298 |
|
299 @property |
|
300 def read_security(self): |
|
301 """return a boolean telling if read security is activated or not""" |
|
302 try: |
|
303 return self._threaddata.read_security |
|
304 except AttributeError: |
|
305 self._threaddata.read_security = self.DEFAULT_SECURITY |
|
306 return self._threaddata.read_security |
|
307 |
|
308 def set_read_security(self, activated): |
|
309 """[de]activate read security, returning the previous value set for |
|
310 later restoration. |
|
311 |
|
312 you should usually use the `security_enabled` context manager instead |
|
313 of this to change security settings. |
|
314 """ |
|
315 oldmode = self.read_security |
|
316 self._threaddata.read_security = activated |
|
317 # dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not |
|
318 # issued on the session). This is tricky since we the execution model of |
|
319 # a (write) user query is: |
|
320 # |
|
321 # repository.execute (security enabled) |
|
322 # \-> querier.execute |
|
323 # \-> repo.glob_xxx (add/update/delete entity/relation) |
|
324 # \-> deactivate security before calling hooks |
|
325 # \-> WE WANT TO CHECK QUERY NATURE HERE |
|
326 # \-> potentially, other calls to querier.execute |
|
327 # |
|
328 # so we can't rely on simply checking session.read_security, but |
|
329 # recalling the first transition from DEFAULT_SECURITY to something |
|
330 # else (False actually) is not perfect but should be enough |
|
331 self._threaddata.dbapi_query = oldmode is self.DEFAULT_SECURITY |
|
332 return oldmode |
|
333 |
|
334 @property |
|
335 def write_security(self): |
|
336 """return a boolean telling if write security is activated or not""" |
|
337 try: |
|
338 return self._threaddata.write_security |
|
339 except: |
|
340 self._threaddata.write_security = self.DEFAULT_SECURITY |
|
341 return self._threaddata.write_security |
|
342 |
|
343 def set_write_security(self, activated): |
|
344 """[de]activate write security, returning the previous value set for |
|
345 later restoration. |
|
346 |
|
347 you should usually use the `security_enabled` context manager instead |
|
348 of this to change security settings. |
|
349 """ |
|
350 oldmode = self.write_security |
|
351 self._threaddata.write_security = activated |
|
352 return oldmode |
|
353 |
|
354 @property |
|
355 def running_dbapi_query(self): |
|
356 """return a boolean telling if it's triggered by a db-api query or by |
|
357 a session query. |
|
358 |
|
359 To be used in hooks, else may have a wrong value. |
|
360 """ |
|
361 return getattr(self._threaddata, 'dbapi_query', True) |
278 |
362 |
279 # hooks activation control ################################################# |
363 # hooks activation control ################################################# |
280 # all hooks should be activated during normal execution |
364 # all hooks should be activated during normal execution |
281 |
365 |
282 HOOKS_ALLOW_ALL = object() |
366 HOOKS_ALLOW_ALL = object() |
503 |
587 |
504 def source_from_eid(self, eid): |
588 def source_from_eid(self, eid): |
505 """return the source where the entity with id <eid> is located""" |
589 """return the source where the entity with id <eid> is located""" |
506 return self.repo.source_from_eid(eid, self) |
590 return self.repo.source_from_eid(eid, self) |
507 |
591 |
508 def decorate_rset(self, rset, propagate=False): |
592 def decorate_rset(self, rset): |
509 rset.vreg = self.vreg |
593 rset.vreg = self.vreg |
510 rset.req = propagate and self or self.actual_session() |
594 rset.req = self |
511 return rset |
595 return rset |
512 |
596 |
513 @property |
597 def execute(self, rql, kwargs=None, eid_key=None, build_descr=True): |
514 def super_session(self): |
598 """db-api like method directly linked to the querier execute method""" |
515 try: |
|
516 csession = self.childsession |
|
517 except AttributeError: |
|
518 if isinstance(self, (ChildSession, InternalSession)): |
|
519 csession = self |
|
520 else: |
|
521 csession = ChildSession(self) |
|
522 self.childsession = csession |
|
523 # need shared pool set |
|
524 self.set_pool(checkclosed=False) |
|
525 return csession |
|
526 |
|
527 def unsafe_execute(self, rql, kwargs=None, eid_key=None, build_descr=True, |
|
528 propagate=False): |
|
529 """like .execute but with security checking disabled (this method is |
|
530 internal to the server, it's not part of the db-api) |
|
531 |
|
532 if `propagate` is true, the super_session will be attached to the result |
|
533 set instead of the parent session, hence further query done through |
|
534 entities fetched from this result set will bypass security as well |
|
535 """ |
|
536 return self.super_session.execute(rql, kwargs, eid_key, build_descr, |
|
537 propagate) |
|
538 |
|
539 def execute(self, rql, kwargs=None, eid_key=None, build_descr=True, |
|
540 propagate=False): |
|
541 """db-api like method directly linked to the querier execute method |
|
542 |
|
543 Becare that unlike actual cursor.execute, `build_descr` default to |
|
544 false |
|
545 """ |
|
546 rset = self._execute(self, rql, kwargs, eid_key, build_descr) |
599 rset = self._execute(self, rql, kwargs, eid_key, build_descr) |
547 return self.decorate_rset(rset, propagate) |
600 return self.decorate_rset(rset) |
548 |
601 |
549 def _clear_thread_data(self): |
602 def _clear_thread_data(self): |
550 """remove everything from the thread local storage, except pool |
603 """remove everything from the thread local storage, except pool |
551 which is explicitly removed by reset_pool, and mode which is set anyway |
604 which is explicitly removed by reset_pool, and mode which is set anyway |
552 by _touch |
605 by _touch |
567 self._touch() |
620 self._touch() |
568 self.debug('commit session %s done (no db activity)', self.id) |
621 self.debug('commit session %s done (no db activity)', self.id) |
569 return |
622 return |
570 if self.commit_state: |
623 if self.commit_state: |
571 return |
624 return |
572 # on rollback, an operation should have the following state |
625 # by default, operations are executed with security turned off |
573 # information: |
626 with security_enabled(self, False, False): |
574 # - processed by the precommit/commit event or not |
627 # on rollback, an operation should have the following state |
575 # - if processed, is it the failed operation |
628 # information: |
576 try: |
629 # - processed by the precommit/commit event or not |
577 for trstate in ('precommit', 'commit'): |
630 # - if processed, is it the failed operation |
578 processed = [] |
631 try: |
579 self.commit_state = trstate |
632 for trstate in ('precommit', 'commit'): |
580 try: |
633 processed = [] |
581 while self.pending_operations: |
634 self.commit_state = trstate |
582 operation = self.pending_operations.pop(0) |
635 try: |
583 operation.processed = trstate |
636 while self.pending_operations: |
584 processed.append(operation) |
637 operation = self.pending_operations.pop(0) |
|
638 operation.processed = trstate |
|
639 processed.append(operation) |
|
640 operation.handle_event('%s_event' % trstate) |
|
641 self.pending_operations[:] = processed |
|
642 self.debug('%s session %s done', trstate, self.id) |
|
643 except: |
|
644 self.exception('error while %sing', trstate) |
|
645 # if error on [pre]commit: |
|
646 # |
|
647 # * set .failed = True on the operation causing the failure |
|
648 # * call revert<event>_event on processed operations |
|
649 # * call rollback_event on *all* operations |
|
650 # |
|
651 # that seems more natural than not calling rollback_event |
|
652 # for processed operations, and allow generic rollback |
|
653 # instead of having to implements rollback, revertprecommit |
|
654 # and revertcommit, that will be enough in mont case. |
|
655 operation.failed = True |
|
656 for operation in processed: |
|
657 operation.handle_event('revert%s_event' % trstate) |
|
658 # XXX use slice notation since self.pending_operations is a |
|
659 # read-only property. |
|
660 self.pending_operations[:] = processed + self.pending_operations |
|
661 self.rollback(reset_pool) |
|
662 raise |
|
663 self.pool.commit() |
|
664 self.commit_state = trstate = 'postcommit' |
|
665 while self.pending_operations: |
|
666 operation = self.pending_operations.pop(0) |
|
667 operation.processed = trstate |
|
668 try: |
585 operation.handle_event('%s_event' % trstate) |
669 operation.handle_event('%s_event' % trstate) |
586 self.pending_operations[:] = processed |
670 except: |
587 self.debug('%s session %s done', trstate, self.id) |
671 self.critical('error while %sing', trstate, |
588 except: |
672 exc_info=sys.exc_info()) |
589 self.exception('error while %sing', trstate) |
673 self.info('%s session %s done', trstate, self.id) |
590 # if error on [pre]commit: |
674 finally: |
591 # |
675 self._clear_thread_data() |
592 # * set .failed = True on the operation causing the failure |
676 self._touch() |
593 # * call revert<event>_event on processed operations |
677 if reset_pool: |
594 # * call rollback_event on *all* operations |
678 self.reset_pool(ignoremode=True) |
595 # |
|
596 # that seems more natural than not calling rollback_event |
|
597 # for processed operations, and allow generic rollback |
|
598 # instead of having to implements rollback, revertprecommit |
|
599 # and revertcommit, that will be enough in mont case. |
|
600 operation.failed = True |
|
601 for operation in processed: |
|
602 operation.handle_event('revert%s_event' % trstate) |
|
603 # XXX use slice notation since self.pending_operations is a |
|
604 # read-only property. |
|
605 self.pending_operations[:] = processed + self.pending_operations |
|
606 self.rollback(reset_pool) |
|
607 raise |
|
608 self.pool.commit() |
|
609 self.commit_state = trstate = 'postcommit' |
|
610 while self.pending_operations: |
|
611 operation = self.pending_operations.pop(0) |
|
612 operation.processed = trstate |
|
613 try: |
|
614 operation.handle_event('%s_event' % trstate) |
|
615 except: |
|
616 self.critical('error while %sing', trstate, |
|
617 exc_info=sys.exc_info()) |
|
618 self.info('%s session %s done', trstate, self.id) |
|
619 finally: |
|
620 self._clear_thread_data() |
|
621 self._touch() |
|
622 if reset_pool: |
|
623 self.reset_pool(ignoremode=True) |
|
624 |
679 |
625 def rollback(self, reset_pool=True): |
680 def rollback(self, reset_pool=True): |
626 """rollback the current session's transaction""" |
681 """rollback the current session's transaction""" |
627 if self.pool is None: |
682 if self.pool is None: |
628 assert not self.pending_operations |
683 assert not self.pending_operations |
629 self._clear_thread_data() |
684 self._clear_thread_data() |
630 self._touch() |
685 self._touch() |
631 self.debug('rollback session %s done (no db activity)', self.id) |
686 self.debug('rollback session %s done (no db activity)', self.id) |
632 return |
687 return |
633 try: |
688 # by default, operations are executed with security turned off |
634 while self.pending_operations: |
689 with security_enabled(self, False, False): |
635 try: |
690 try: |
636 operation = self.pending_operations.pop(0) |
691 while self.pending_operations: |
637 operation.handle_event('rollback_event') |
692 try: |
638 except: |
693 operation = self.pending_operations.pop(0) |
639 self.critical('rollback error', exc_info=sys.exc_info()) |
694 operation.handle_event('rollback_event') |
640 continue |
695 except: |
641 self.pool.rollback() |
696 self.critical('rollback error', exc_info=sys.exc_info()) |
642 self.debug('rollback for session %s done', self.id) |
697 continue |
643 finally: |
698 self.pool.rollback() |
644 self._clear_thread_data() |
699 self.debug('rollback for session %s done', self.id) |
645 self._touch() |
700 finally: |
646 if reset_pool: |
701 self._clear_thread_data() |
647 self.reset_pool(ignoremode=True) |
702 self._touch() |
|
703 if reset_pool: |
|
704 self.reset_pool(ignoremode=True) |
648 |
705 |
649 def close(self): |
706 def close(self): |
650 """do not close pool on session close, since they are shared now""" |
707 """do not close pool on session close, since they are shared now""" |
651 self._closed = True |
708 self._closed = True |
652 # copy since _threads_in_transaction maybe modified while waiting |
709 # copy since _threads_in_transaction maybe modified while waiting |
791 def entity(self, eid): |
867 def entity(self, eid): |
792 """return a result set for the given eid""" |
868 """return a result set for the given eid""" |
793 return self.entity_from_eid(eid) |
869 return self.entity_from_eid(eid) |
794 |
870 |
795 |
871 |
796 class ChildSession(Session): |
|
797 """child (or internal) session are used to hijack the security system |
|
798 """ |
|
799 cnxtype = 'inmemory' |
|
800 |
|
801 def __init__(self, parent_session): |
|
802 self.id = None |
|
803 self.is_internal_session = False |
|
804 self.is_super_session = True |
|
805 # session which has created this one |
|
806 self.parent_session = parent_session |
|
807 self.user = InternalManager() |
|
808 self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone |
|
809 self.repo = parent_session.repo |
|
810 self.vreg = parent_session.vreg |
|
811 self.data = parent_session.data |
|
812 self.encoding = parent_session.encoding |
|
813 self.lang = parent_session.lang |
|
814 self._ = self.__ = parent_session._ |
|
815 # short cut to querier .execute method |
|
816 self._execute = self.repo.querier.execute |
|
817 |
|
818 @property |
|
819 def super_session(self): |
|
820 return self |
|
821 |
|
822 @property |
|
823 def hooks_mode(self): |
|
824 return self.parent_session.hooks_mode |
|
825 def set_hooks_mode(self, mode): |
|
826 return self.parent_session.set_hooks_mode(mode) |
|
827 |
|
828 @property |
|
829 def disabled_hooks_categories(self): |
|
830 return self.parent_session.disabled_hooks_categories |
|
831 |
|
832 @property |
|
833 def enabled_hooks_categories(self): |
|
834 return self.parent_session.enabled_hooks_categories |
|
835 |
|
836 |
|
837 def get_mode(self): |
|
838 return self.parent_session.mode |
|
839 def set_mode(self, value): |
|
840 self.parent_session.set_mode(value) |
|
841 mode = property(get_mode, set_mode) |
|
842 |
|
843 def get_commit_state(self): |
|
844 return self.parent_session.commit_state |
|
845 def set_commit_state(self, value): |
|
846 self.parent_session.set_commit_state(value) |
|
847 commit_state = property(get_commit_state, set_commit_state) |
|
848 |
|
849 @property |
|
850 def pool(self): |
|
851 return self.parent_session.pool |
|
852 @property |
|
853 def pending_operations(self): |
|
854 return self.parent_session.pending_operations |
|
855 @property |
|
856 def transaction_data(self): |
|
857 return self.parent_session.transaction_data |
|
858 |
|
859 def set_pool(self): |
|
860 """the session need a pool to execute some queries""" |
|
861 self.parent_session.set_pool() |
|
862 |
|
863 def reset_pool(self): |
|
864 """the session has no longer using its pool, at least for some time |
|
865 """ |
|
866 self.parent_session.reset_pool() |
|
867 |
|
868 def actual_session(self): |
|
869 """return the original parent session if any, else self""" |
|
870 return self.parent_session |
|
871 |
|
872 def commit(self, reset_pool=True): |
|
873 """commit the current session's transaction""" |
|
874 self.parent_session.commit(reset_pool) |
|
875 |
|
876 def rollback(self, reset_pool=True): |
|
877 """rollback the current session's transaction""" |
|
878 self.parent_session.rollback(reset_pool) |
|
879 |
|
880 def close(self): |
|
881 """do not close pool on session close, since they are shared now""" |
|
882 self.rollback() |
|
883 |
|
884 def user_data(self): |
|
885 """returns a dictionnary with this user's information""" |
|
886 return self.parent_session.user_data() |
|
887 |
|
888 |
|
889 class InternalSession(Session): |
872 class InternalSession(Session): |
890 """special session created internaly by the repository""" |
873 """special session created internaly by the repository""" |
891 |
874 |
892 def __init__(self, repo, cnxprops=None): |
875 def __init__(self, repo, cnxprops=None): |
893 super(InternalSession, self).__init__(InternalManager(), repo, cnxprops, |
876 super(InternalSession, self).__init__(InternalManager(), repo, cnxprops, |
894 _id='internal') |
877 _id='internal') |
895 self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone |
878 self.user.req = self # XXX remove when "vreg = user.req.vreg" hack in entity.py is gone |
896 self.cnxtype = 'inmemory' |
879 self.cnxtype = 'inmemory' |
897 self.is_internal_session = True |
880 self.is_internal_session = True |
898 self.is_super_session = True |
881 self.disable_hooks_category('integrity') |
899 |
|
900 @property |
|
901 def super_session(self): |
|
902 return self |
|
903 |
882 |
904 |
883 |
905 class InternalManager(object): |
884 class InternalManager(object): |
906 """a manager user with all access rights used internally for task such as |
885 """a manager user with all access rights used internally for task such as |
907 bootstrapping the repository or creating regular users according to |
886 bootstrapping the repository or creating regular users according to |