merge
authorsylvain.thenault@logilab.fr
Tue, 13 Jan 2009 17:58:58 +0100
changeset 398 cc16ee7d2d86
parent 396 76d593bd4221 (diff)
parent 397 cf577e26f924 (current diff)
child 399 8df3b190275b
child 403 ffc7dc7d2218
merge
--- a/common/selectors.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/common/selectors.py	Tue Jan 13 17:58:58 2009 +0100
@@ -410,7 +410,8 @@
         if not (eschema.has_perm(req, perm) or eschema.has_local_role(perm)):
             return 0
     if hasattr(cls, 'rtype'):
-        if not schema.rschema(cls.rtype).has_perm(req, perm):
+        rschema = schema.rschema(cls.rtype)
+        if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
             return 0
     return 1
 
@@ -421,8 +422,9 @@
     result set has this relation.
     """
     if hasattr(cls, 'rtype'):
+        rschema = cls.schema.rschema(cls.rtype)
         perm = getattr(cls, 'require_permission', 'read')
-        if not cls.schema.rschema(cls.rtype).has_perm(req, perm):
+        if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
             return 0
         if row is None:
             for etype in rset.column_types(col or 0):
@@ -439,8 +441,9 @@
     .rtype attribute of the class, and if at least one entity type in the
     result set has this relation.
     """
+    rschema = cls.schema.rschema(cls.rtype)
     perm = getattr(cls, 'require_permission', 'read')
-    if not cls.schema.rschema(cls.rtype).has_perm(req, perm):
+    if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
         return 0
     if row is None:
         for etype in rset.column_types(col or 0):
--- a/cwvreg.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/cwvreg.py	Tue Jan 13 17:58:58 2009 +0100
@@ -201,7 +201,10 @@
                     yield view
             except NoSelectableObject:
                 continue
-            
+            except Exception:
+                self.exception('error while trying to list possible %s views for %s',
+                               vid, rset)
+                
     def select_box(self, oid, *args, **kwargs):
         """return the most specific view according to the result set"""
         try:
--- a/devtools/devctl.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/devtools/devctl.py	Tue Jan 13 17:58:58 2009 +0100
@@ -35,8 +35,11 @@
         if cube is None:
             self._cubes = ()
         else:
-            self._cubes = self.expand_cubes((cube,))
-        
+            self._cubes = self.expand_cubes(self.my_cubes(cube))
+
+    def my_cubes(self, cube):
+        return (cube,) + self.cube_dependencies(cube) + self.cube_recommends(cube)
+    
     @property
     def apphome(self):
         return None
@@ -52,12 +55,9 @@
     """configuration to use to generate cubicweb po files or to use as "library" configuration
     to filter out message ids from cubicweb and dependencies of a cube
     """
-    def __init__(self, cube=None):
-        super(DevDepConfiguration, self).__init__(cube)
-        if cube is None:
-            self._cubes = ()
-        else:
-            self._cubes = self.expand_cubes(self.cube_dependencies(cube))
+
+    def my_cubes(self, cube):
+        return self.cube_dependencies(cube) + self.cube_recommends(cube)
 
     def default_log_file(self):
         return None
--- a/server/msplanner.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/server/msplanner.py	Tue Jan 13 17:58:58 2009 +0100
@@ -54,6 +54,7 @@
 """
 __docformat__ = "restructuredtext en"
 
+from copy import deepcopy
 from itertools import imap, ifilterfalse
 
 from logilab.common.compat import any
@@ -75,6 +76,7 @@
 Constant._ms_table_key = lambda x: str(x.value)
 
 AbstractSource.dont_cross_relations = ()
+AbstractSource.cross_relations = ()
 
 def allequals(solutions):
     """return true if all solutions are identical"""
@@ -172,15 +174,23 @@
         self._session = plan.session
         self._solutions = rqlst.solutions
         self._solindices = range(len(self._solutions))
-        # source : {varname: [solution index, ]}
-        self._sourcesvars = {}
+        # source : {var: [solution index, ]}
+        self.sourcesvars = self._sourcesvars = {}
+        # source : {relation: set(child variable and constant)}
+        self._crossrelations = {}
         # dictionnary of variables which are linked to each other using a non
         # final relation which is supported by multiple sources
         self._linkedvars = {}
+        self._crosslinkedvars = {}
         # processing
         self._compute_sourcesvars()
         self._remove_invalid_sources()
         self._compute_needsplit()
+        self.sourcesvars = {}
+        for k, v in self._sourcesvars.iteritems():
+            self.sourcesvars[k] = {}
+            for k2, v2 in v.iteritems():
+                self.sourcesvars[k][k2] = v2.copy()
         self._inputmaps = {}
         if rqlhelper is not None: # else test
             self._insert_identity_variable = rqlhelper._annotator.rewrite_shared_optional
@@ -202,16 +212,17 @@
                          for solindex in self._solindices)        
        
     @cached
-    def _norel_support_set(self, rtype):
+    def _norel_support_set(self, relation):
         """return a set of (source, solindex) where source doesn't support the
         relation
         """
         return frozenset((source, solidx) for source in self._session.repo.sources
                          for solidx in self._solindices
-                         if not (source.support_relation(rtype)
-                                 or rtype in source.dont_cross_relations))
-        
-    
+                         if not ((source.support_relation(relation.r_type) and
+                                  not self.crossed_relation(source, relation))
+                                 or relation.r_type in source.dont_cross_relations))
+
+
     def _compute_sourcesvars(self):
         """compute for each variable/solution in the rqlst which sources support
         them
@@ -264,9 +275,50 @@
                         if not varobj._q_invariant and any(ifilterfalse(
                             source.support_relation, (r.r_type for r in rels))):
                             self.needsplit = True               
-            
+
+    def _handle_cross_relation(self, rel, relsources, vsources):
+        crossvars = None
+        for source in relsources:
+            if rel.r_type in source.cross_relations:
+                crossvars = set(x.variable for x in rel.get_nodes(VariableRef))
+                crossvars.update(frozenset(x for x in rel.get_nodes(Constant)))
+                assert len(crossvars) == 2
+                ssource = self._session.repo.system_source
+                needsplit = True
+                flag = 0
+                for v in crossvars:
+                    if isinstance(v, Constant):
+                        self._sourcesvars[ssource][v] = set(self._solindices)
+                    if len(vsources[v]) == 1:
+                        if iter(vsources[v]).next()[0].uri == 'system':
+                            flag = 1
+                            for ov in crossvars:
+                                if ov is not v and ov._q_invariant:
+                                    ssset = frozenset((ssource,))
+                                    self._remove_sources(ov, vsources[ov] - ssset)
+                        else:
+                            for ov in crossvars:
+                                if ov is not v and ov._q_invariant:
+                                    needsplit = False
+                                    break
+                            else:
+                                continue
+                        if not rel.neged(strict=True):
+                            break
+                else:
+                    self._crossrelations.setdefault(source, {})[rel] = crossvars
+                    if not flag:
+                        self._sourcesvars.setdefault(source, {})[rel] = set(self._solindices)
+                    self._sourcesvars.setdefault(ssource, {})[rel] = set(self._solindices)
+                    if needsplit:
+                        self.needsplit = True
+        return crossvars is None
+        
     def _remove_invalid_sources(self):
-        """removes invalid sources from `sourcesvars` member"""
+        """removes invalid sources from `sourcesvars` member according to
+        traversed relations and their properties (which sources support them,
+        can they cross sources, etc...)
+        """
         repo = self._session.repo
         rschema = repo.schema.rschema
         vsources = {}
@@ -276,8 +328,18 @@
             # during bootstrap)
             if not rel.is_types_restriction() and not rschema(rel.r_type).is_final():
                 # nothing to do if relation is not supported by multiple sources
+                # or if some source has it listed in its cross_relations
+                # attribute
+                #
+                # XXX code below don't deal if some source allow relation
+                #     crossing but not another one
                 relsources = repo.rel_type_sources(rel.r_type)
+                crossvars = None
                 if len(relsources) < 2:
+                    # filter out sources being there because they have this
+                    # relation in their dont_cross_relations attribute
+                    relsources = [source for source in relsources
+                                  if source.support_relation(rel.r_type)]
                     if relsources:
                         # this means the relation is using a variable inlined as
                         # a constant and another unsupported variable, in which
@@ -291,8 +353,12 @@
                     vsources[lhsv] = self._term_sources(lhs)
                 if not rhsv in vsources:
                     vsources[rhsv] = self._term_sources(rhs)
-                self._linkedvars.setdefault(lhsv, set()).add((rhsv, rel))
-                self._linkedvars.setdefault(rhsv, set()).add((lhsv, rel))
+                if self._handle_cross_relation(rel, relsources, vsources):
+                    self._linkedvars.setdefault(lhsv, set()).add((rhsv, rel))
+                    self._linkedvars.setdefault(rhsv, set()).add((lhsv, rel))
+                else:
+                    self._crosslinkedvars.setdefault(lhsv, set()).add((rhsv, rel))
+                    self._crosslinkedvars.setdefault(rhsv, set()).add((lhsv, rel))
         for term in self._linkedvars:
             self._remove_sources_until_stable(term, vsources)
         if len(self._sourcesvars) > 1 and hasattr(self.plan.rqlst, 'main_relations'):
@@ -308,10 +374,7 @@
             for rel in self.plan.rqlst.main_relations:
                 if not rschema(rel.r_type).is_final():
                     # nothing to do if relation is not supported by multiple sources
-                    relsources = [source for source in repo.sources
-                                  if source.support_relation(rel.r_type)
-                                  or rel.r_type in source.dont_cross_relations]
-                    if len(relsources) < 2:
+                    if len(repo.rel_type_sources(rel.r_type)) < 2:
                         continue
                     lhs, rhs = rel.get_variable_parts()
                     try:
@@ -319,7 +382,7 @@
                         rhsv = self._extern_term(rhs, vsources, inserted)
                     except KeyError, ex:
                         continue
-                    norelsup = self._norel_support_set(rel.r_type)
+                    norelsup = self._norel_support_set(rel)
                     self._remove_var_sources(lhsv, norelsup, rhsv, vsources)
                     self._remove_var_sources(rhsv, norelsup, lhsv, vsources)
         # cleanup linked var
@@ -364,6 +427,8 @@
                 # can't get information from relation inside a NOT exists
                 # where variables don't belong to the same scope
                 continue
+            if not (var.scope is rel.scope and ovar.scope is rel.scope) and rel.ored():
+                continue
             relsources = self._session.repo.rel_type_sources(rel.r_type)
             if rel.neged(strict=True) and (
                 len(relsources) < 2
@@ -377,7 +442,7 @@
                 # on a multisource relation for a variable only used by this relation
                 # (eg "Any X WHERE NOT X multisource_rel Y" and over is Y), iif 
                 continue
-            norelsup = self._norel_support_set(rel.r_type)
+            norelsup = self._norel_support_set(rel)
             # compute invalid sources for variables and remove them
             self._remove_var_sources(var, norelsup, ovar, vsources)
             self._remove_var_sources(ovar, norelsup, var, vsources)
@@ -401,7 +466,7 @@
         * a source support an entity (non invariant) but doesn't support a
           relation on it
         * a source support an entity which is accessed by an optional relation
-        * there is more than one sources and either all sources'supported        
+        * there is more than one source and either all sources'supported        
           variable/solutions are not equivalent or multiple variables have to
           be fetched from some source
         """
@@ -414,9 +479,10 @@
             else:
                 sample = self._sourcesvars.itervalues().next()
                 if len(sample) > 1 and any(v for v in sample
-                                           if not v in self._linkedvars):
+                                           if not v in self._linkedvars
+                                           and not v in self._crosslinkedvars):
                     self.needsplit = True
-
+            
     def _set_source_for_var(self, source, var):
         self._sourcesvars.setdefault(source, {})[var] = set(self._solindices)
 
@@ -427,10 +493,10 @@
             return set((source, solindex) for solindex in self._solindices)
         else:
             var = getattr(term, 'variable', term)
-            sources = [source for source, varobjs in self._sourcesvars.iteritems()
+            sources = [source for source, varobjs in self.sourcesvars.iteritems()
                        if var in varobjs]
             return set((source, solindex) for source in sources
-                       for solindex in self._sourcesvars[source][var])
+                       for solindex in self.sourcesvars[source][var])
 
     def _remove_sources(self, var, sources):
         """removes invalid sources (`sources`) from `sourcesvars`
@@ -449,6 +515,9 @@
                 if not sourcesvars[source]:
                     del sourcesvars[source]
 
+    def crossed_relation(self, source, relation):
+        return relation in self._crossrelations.get(source, ())
+    
     def part_steps(self):
         """precompute necessary part steps before generating actual rql for
         each step. This is necessary to know if an aggregate step will be
@@ -472,7 +541,7 @@
                     sourcevars.clear()
                 else:
                     scope = var.scope
-                    variables = self._expand_vars(var, sourcevars, scope, solindices)
+                    variables = self._expand_vars(var, source, sourcevars, scope, solindices)
                     if not sourcevars:
                         del self._sourcesvars[source]
                 # find which sources support the same variables/solutions
@@ -502,11 +571,15 @@
                         eid = const.eval(self.plan.args)
                         _source = self._session.source_from_eid(eid)
                         if len(sources) > 1 or not _source in sources:
-                            # if constant is only used by an identity relation,
-                            # skip
+                            # if there is some rewriten constant used by a
+                            # not neged relation while there are some source
+                            # not supporting the associated entity, this step
+                            # can't be final (unless the relation is explicitly
+                            # in `variables`, eg cross relations)
                             for c in vconsts:
                                 rel = c.relation()
-                                if rel is None or not rel.neged(strict=True):
+                                if rel is None or not (rel in variables or rel.neged(strict=True)):
+                                #if rel is not None and rel.r_type == 'identity' and not rel.neged(strict=True):
                                     final = False
                                     break
                             break
@@ -521,6 +594,9 @@
                                 needsel.add(vref.name)
                             final = False
                             break
+                        elif self.crossed_relation(_source, rel) and not rel in variables:
+                            final = False
+                            break
                     else:
                         if not scope is select:
                             self._exists_relation(rel, variables, needsel)
@@ -576,8 +652,6 @@
             relation.children[1].operator = '=' 
             variables.append(newvar)
             needsel.add(newvar.name)
-            #self.insertedvars.append((var.name, self.schema['identity'],
-            #                          newvar.name))
         
     def _choose_var(self, sourcevars):
         secondchoice = None
@@ -587,7 +661,7 @@
                 if not var.scope is self.rqlst:
                     if isinstance(var, Variable):
                         return var, sourcevars.pop(var)
-                    secondchoice = var
+                    secondchoice = var, sourcevars.pop(var)
         else:
             # priority to variable outer scope
             for var in sourcevars:
@@ -605,7 +679,8 @@
         var = iter(sourcevars).next()
         return var, sourcevars.pop(var)
             
-    def _expand_vars(self, var, sourcevars, scope, solindices):
+            
+    def _expand_vars(self, var, source, sourcevars, scope, solindices):
         variables = [var]
         nbunlinked = 1
         linkedvars = self._linkedvars
@@ -615,28 +690,41 @@
             candidates = (v for v in sourcevars.keys() if scope is v.scope)
         else:
             candidates = sourcevars #.iterkeys()
+        # we only want one unlinked variable in each generated query
         candidates = [v for v in candidates
                       if isinstance(v, Constant) or
                       (solindices.issubset(sourcevars[v]) and v in linkedvars)]
+        accept_var = lambda x: (isinstance(x, Constant) or any(v for v in variables if v in linkedvars.get(x, ())))
+        source_cross_rels = self._crossrelations.get(source, ())
+        if isinstance(var, Relation) and var in source_cross_rels:
+            cross_vars = source_cross_rels.pop(var)
+            base_accept_var = accept_var
+            accept_var = lambda x: (base_accept_var(x) or x in cross_vars)
+            for refed in cross_vars:
+                if not refed in candidates:
+                    candidates.append(refed)
+        else:
+            cross_vars = ()
         # repeat until no variable can't be added, since addition of a new
         # variable may permit to another one to be added
         modified = True
         while modified and candidates:
             modified = False
             for var in candidates[:]:
-                # we only want one unlinked variable in each generated query
-                if isinstance(var, Constant) or \
-                       any(v for v in variables if v in linkedvars[var]):
+                if accept_var(var):
                     variables.append(var)
-                    # constant nodes should be systematically deleted
-                    if isinstance(var, Constant):
-                        del sourcevars[var]
-                    # variable nodes should be deleted once all possible solution
-                    # indices have been consumed
-                    else:
-                        sourcevars[var] -= solindices
-                        if not sourcevars[var]:
+                    try:
+                        # constant nodes should be systematically deleted
+                        if isinstance(var, Constant):
                             del sourcevars[var]
+                        else:
+                            # variable nodes should be deleted once all possible
+                            # solutions indices have been consumed
+                            sourcevars[var] -= solindices
+                            if not sourcevars[var]:
+                                del sourcevars[var]
+                    except KeyError:
+                        assert var in cross_vars
                     candidates.remove(var)
                     modified = True
         return variables
@@ -658,21 +746,22 @@
                         varsolindices = sourcesvars[source][var]
                         varsolindices -= solindices
                         if not varsolindices:
-                            del sourcesvars[source][var]
-                
+                            del sourcesvars[source][var]                
         return sources
     
     def _cleanup_sourcesvars(self, sources, solindices):
         """on final parts, remove solutions so we know they are already processed"""
         for source in sources:
             try:
-                sourcevar = self._sourcesvars[source]
+                sourcevars = self._sourcesvars[source]
             except KeyError:
                 continue
-            for var, varsolindices in sourcevar.items():
+            for var, varsolindices in sourcevars.items():
+                if isinstance(var, Relation) and self.crossed_relation(source, var):
+                    continue
                 varsolindices -= solindices
                 if not varsolindices:
-                    del sourcevar[var]
+                    del sourcevars[var]
                     
     def merge_input_maps(self, allsolindices):
         """inputmaps is a dictionary with tuple of solution indices as key with an
@@ -952,7 +1041,8 @@
         if server.DEBUG:
             print 'filter', final and 'final' or '', sources, variables, rqlst, solindices, needsel
         newroot = Select()
-        self.sources = sources
+        self.sources = sorted(sources)
+        self.variables = variables
         self.solindices = solindices
         self.final = final
         # variables which appear in unsupported branches
@@ -1065,10 +1155,20 @@
 
     visit_or = visit_and
 
-    def _relation_supported(self, rtype):
+    def _relation_supported(self, relation):
+        rtype = relation.r_type
         for source in self.sources:
-            if not source.support_relation(rtype):
+            if not source.support_relation(rtype) \
+                   or (rtype in source.cross_relations and not relation in self.variables):#self.ppi.crossed_relation(source, relation):
                 return False
+        if not self.final:
+            rschema = self.schema.rschema(relation.r_type)
+            if not rschema.is_final():
+                for term in relation.get_nodes((VariableRef, Constant)):
+                    term = getattr(term, 'variable', term)
+                    termsources = sorted(set(x[0] for x in self.ppi._term_sources(term)))
+                    if termsources and termsources != self.sources:
+                        return False
         return True
         
     def visit_relation(self, node, newroot, variables):
@@ -1085,12 +1185,12 @@
                         return None, node
                 else:
                     return None, node
-            if not self._relation_supported(node.r_type):
+            if not self._relation_supported(node):
                 raise UnsupportedBranch()
         # don't copy type restriction unless this is the only relation for the
         # rhs variable, else they'll be reinserted later as needed (else we may
         # copy a type restriction while the variable is not actually used)
-        elif not any(self._relation_supported(rel.r_type)
+        elif not any(self._relation_supported(rel)
                      for rel in node.children[0].variable.stinfo['relations']):
             rel, node = self.visit_default(node, newroot, variables)
             return rel, node
@@ -1105,8 +1205,7 @@
                 if not ored:
                     self.skip.setdefault(node, set()).update(self.solindices)
                 else:
-                    self.mayneedvar.setdefault((node.children[0].name, rschema), []).append( (res, ored) )
-                    
+                    self.mayneedvar.setdefault((node.children[0].name, rschema), []).append( (res, ored) )                    
             else:
                 assert len(vrefs) == 1
                 vref = vrefs[0]
--- a/server/querier.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/server/querier.py	Tue Jan 13 17:58:58 2009 +0100
@@ -44,6 +44,7 @@
     for rel in restriction.iget_nodes(Relation):
         cmp = rel.children[1]
         if rel.r_type == 'eid' and cmp.operator == '=' and \
+               not rel.neged(strict=True) and \
                isinstance(cmp.children[0], Constant) and \
                cmp.children[0].type == 'Substitute':
             varkwargs[rel.children[0].name] = typed_eid(cmp.children[0].eval(args))
--- a/server/sources/pyrorql.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/server/sources/pyrorql.py	Tue Jan 13 17:58:58 2009 +0100
@@ -341,7 +341,8 @@
     """translate a local rql query to be executed on a distant repository"""
     def __init__(self, source):
         self.source = source
-
+        self.current_operator = None
+        
     def _accept_children(self, node):
         res = []
         for child in node.children:
@@ -428,7 +429,13 @@
         try:
             if isinstance(node.children[0], Constant):
                 # simplified rqlst, reintroduce eid relation
-                restr, lhs = self.process_eid_const(node.children[0])
+                try:
+                    restr, lhs = self.process_eid_const(node.children[0])
+                except UnknownEid:
+                    # can safely skip not relation with an unsupported eid
+                    if node.neged(strict=True):
+                        return
+                    raise
             else:
                 lhs = node.children[0].accept(self)
                 restr = None
--- a/server/test/data/schema/custom.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/server/test/data/schema/custom.py	Tue Jan 13 17:58:58 2009 +0100
@@ -15,6 +15,10 @@
     subject = ('Card', 'Note')
     object = 'Note'
 
+class multisource_crossed_rel(RelationDefinition):
+    subject = ('Card', 'Note')
+    object = 'Note'
+
 class multisource_inlined_rel(RelationType):
     inlined = True
     cardinality = '?*'
--- a/server/test/unittest_msplanner.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/server/test/unittest_msplanner.py	Tue Jan 13 17:58:58 2009 +0100
@@ -28,8 +28,10 @@
 class FakeCardSource(AbstractSource):
     uri = 'ccc'
     support_entities = {'Card': True, 'Note': True, 'State': True}
-    support_relations = {'in_state': True, 'multisource_rel': True, 'multisource_inlined_rel': True}
+    support_relations = {'in_state': True, 'multisource_rel': True, 'multisource_inlined_rel': True,
+                         'multisource_crossed_rel': True}
     dont_cross_relations = set(('fiche',))
+    cross_relations = set(('multisource_crossed_rel',))
     
     def syntax_tree_search(self, *args, **kwargs):
         return []
@@ -1046,6 +1048,32 @@
                      None, None, [self.system],
                      {'T': 'table0.C0', 'T2': 'table1.C0',
                       'X': 'table2.C1', 'X.login': 'table2.C0', 'L': 'table2.C0'}, [])])
+            
+    def test_exists_security_no_invariant(self):
+        ueid = self.session.user.eid
+        self._test('Any X,AA,AB,AC,AD ORDERBY AA WHERE X is EUser, X login AA, X firstname AB, X surname AC, X modification_date AD, A eid %(B)s, \
+    EXISTS(((X identity A) OR \
+            (EXISTS(X in_group C, C name IN("managers", "staff"), C is EGroup))) OR \
+            (EXISTS(X in_group D, A in_group D, NOT D name "users", D is EGroup)))',
+               [('FetchStep', [('Any X,AA,AB,AC,AD WHERE X login AA, X firstname AB, X surname AC, X modification_date AD, X is EUser',
+                                [{'AA': 'String', 'AB': 'String', 'AC': 'String', 'AD': 'Datetime',
+                                  'X': 'EUser'}])],
+                 [self.ldap, self.system], None, {'AA': 'table0.C1', 'AB': 'table0.C2',
+                                                  'AC': 'table0.C3', 'AD': 'table0.C4',
+                                                  'X': 'table0.C0',
+                                                  'X.firstname': 'table0.C2',
+                                                  'X.login': 'table0.C1',
+                                                  'X.modification_date': 'table0.C4',
+                                                  'X.surname': 'table0.C3'}, []),
+                ('OneFetchStep', [('Any X,AA,AB,AC,AD ORDERBY AA WHERE X login AA, X firstname AB, X surname AC, X modification_date AD, EXISTS(((X identity 5) OR (EXISTS(X in_group C, C name IN("managers", "staff"), C is EGroup))) OR (EXISTS(X in_group D, 5 in_group D, NOT D name "users", D is EGroup))), X is EUser',
+                                   [{'AA': 'String', 'AB': 'String', 'AC': 'String', 'AD': 'Datetime',
+                                     'C': 'EGroup', 'D': 'EGroup', 'X': 'EUser'}])],
+                 None, None, [self.system],
+                 {'AA': 'table0.C1', 'AB': 'table0.C2', 'AC': 'table0.C3', 'AD': 'table0.C4',
+                  'X': 'table0.C0',
+                  'X.firstname': 'table0.C2', 'X.login': 'table0.C1', 'X.modification_date': 'table0.C4', 'X.surname': 'table0.C3'},
+                 [])],
+                   {'B': ueid})
 
     def test_relation_need_split(self):
         self._test('Any X, S WHERE X in_state S',
@@ -1338,6 +1366,93 @@
                                        [{'X': 'Personne', 'Y': 'Card', 'YT': 'String'}])],
                      None, None, [self.system], {}, [])],
                    {'x': 999999})
+
+
+    # external source w/ .cross_relations == ['multisource_crossed_rel'] ######
+    
+    def test_crossed_relation_eid_1_invariant(self):
+        repo._type_source_cache[999999] = ('Note', 'system', 999999)
+        ueid = self.session.user.eid
+        self._test('Any Y WHERE X eid %(x)s, X multisource_crossed_rel Y',
+                   [('OneFetchStep', [('Any Y WHERE 999999 multisource_crossed_rel Y', [{u'Y': 'Note'}])],
+                      None, None, [self.system], {}, [])
+                    ],
+                   {'x': 999999,})
+
+    def test_crossed_relation_eid_1_needattr(self):
+        repo._type_source_cache[999999] = ('Note', 'system', 999999)
+        ueid = self.session.user.eid
+        self._test('Any Y,T WHERE X eid %(x)s, X multisource_crossed_rel Y, Y type T',
+                   [('FetchStep', [('Any Y,T WHERE Y type T, Y is Note', [{'T': 'String', 'Y': 'Note'}])],
+                     [self.rql, self.system], None,
+                     {'T': 'table0.C1', 'Y': 'table0.C0', 'Y.type': 'table0.C1'}, []),
+                    ('OneFetchStep', [('Any Y,T WHERE 999999 multisource_crossed_rel Y, Y type T, Y is Note',
+                                       [{'T': 'String', 'Y': 'Note'}])],
+                     None, None, [self.system],
+                     {'T': 'table0.C1', 'Y': 'table0.C0', 'Y.type': 'table0.C1'}, []),
+                    ],
+                   {'x': 999999,})
+
+    def test_crossed_relation_eid_2_invariant(self):
+        repo._type_source_cache[999999] = ('Note', 'cards', 999999)
+        ueid = self.session.user.eid
+        self._test('Any Y WHERE X eid %(x)s, X multisource_crossed_rel Y',
+                   [('OneFetchStep', [('Any Y WHERE 999999 multisource_crossed_rel Y, Y is Note', [{'Y': 'Note'}])],
+                      None, None, [self.rql, self.system], {}, [])
+                    ],
+                   {'x': 999999,})
+
+    def test_crossed_relation_eid_2_needattr(self):
+        repo._type_source_cache[999999] = ('Note', 'cards', 999999)
+        ueid = self.session.user.eid
+        self._test('Any Y,T WHERE X eid %(x)s, X multisource_crossed_rel Y, Y type T',
+                   [('FetchStep', [('Any Y,T WHERE Y type T, Y is Note', [{'T': 'String', 'Y': 'Note'}])],
+                     [self.rql, self.system], None,
+                     {'T': 'table0.C1', 'Y': 'table0.C0', 'Y.type': 'table0.C1'}, []),
+                    ('OneFetchStep', [('Any Y,T WHERE 999999 multisource_crossed_rel Y, Y type T, Y is Note',
+                                       [{'T': 'String', 'Y': 'Note'}])],
+                     None, None, [self.rql, self.system],
+                     {'T': 'table0.C1', 'Y': 'table0.C0', 'Y.type': 'table0.C1'},
+                     [])
+                    ],
+                   {'x': 999999,})
+
+    def test_crossed_relation_eid_not_1(self):
+        repo._type_source_cache[999999] = ('Note', 'system', 999999)
+        ueid = self.session.user.eid
+        self._test('Any Y WHERE X eid %(x)s, NOT X multisource_crossed_rel Y',
+                   [('FetchStep', [('Any Y WHERE Y is Note', [{'Y': 'Note'}])],
+                     [self.rql, self.system], None, {'Y': 'table0.C0'}, []),
+                    ('OneFetchStep', [('Any Y WHERE NOT 999999 multisource_crossed_rel Y, Y is Note',
+                                       [{'Y': 'Note'}])],
+                     None, None, [self.system],
+                     {'Y': 'table0.C0'},  [])],
+                   {'x': 999999,})
+
+#     def test_crossed_relation_eid_not_2(self):
+#         repo._type_source_cache[999999] = ('Note', 'cards', 999999)
+#         ueid = self.session.user.eid
+#         self._test('Any Y WHERE X eid %(x)s, NOT X multisource_crossed_rel Y',
+#                    [],
+#                    {'x': 999999,})
+
+    def test_crossed_relation_base(self):
+        repo._type_source_cache[999999] = ('Note', 'system', 999999)
+        ueid = self.session.user.eid
+        self._test('Any X,Y,T WHERE X multisource_crossed_rel Y, Y type T, X type T',
+                   [('FetchStep', [('Any X,T WHERE X type T, X is Note', [{'T': 'String', 'X': 'Note'}])],
+                     [self.rql, self.system], None,
+                     {'T': 'table0.C1', 'X': 'table0.C0', 'X.type': 'table0.C1'}, []),
+                    ('FetchStep',  [('Any Y,T WHERE Y type T, Y is Note', [{'T': 'String', 'Y': 'Note'}])],
+                     [self.rql, self.system], None,
+                     {'T': 'table1.C1', 'Y': 'table1.C0', 'Y.type': 'table1.C1'},  []),
+                    ('OneFetchStep', [('Any X,Y,T WHERE X multisource_crossed_rel Y, Y type T, X type T, X is Note, Y is Note',
+                                       [{'T': 'String', 'X': 'Note', 'Y': 'Note'}])],
+                     None, None, [self.rql, self.system],
+                     {'T': 'table1.C1', 'X': 'table0.C0', 'X.type': 'table0.C1',
+                      'Y': 'table1.C0', 'Y.type': 'table1.C1'},
+                    [])],
+                    {'x': 999999,})
         
     # edition queries tests ###################################################
 
--- a/server/test/unittest_security.py	Mon Jan 12 18:15:14 2009 +0100
+++ b/server/test/unittest_security.py	Tue Jan 13 17:58:58 2009 +0100
@@ -253,7 +253,7 @@
         self.assertEquals(rset.rows, [[aff2]])
         # more cache test w/ NOT eid
         rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid}, 'x')
-        self.assertEquals(rset.rows, [])
+        self.assertEquals(rset.rows, [[aff2]])
         rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2}, 'x')
         self.assertEquals(rset.rows, [])