schema.py
changeset 9393 8266c8c375bb
parent 9366 bcbc92223b35
child 9395 96dba2efd16d
--- a/schema.py	Fri Jan 10 15:02:07 2014 +0100
+++ b/schema.py	Fri Jan 10 16:25:45 2014 +0100
@@ -104,6 +104,297 @@
 ybo.ETYPE_PROPERTIES += ('eid',)
 ybo.RTYPE_PROPERTIES += ('eid',)
 
+# Bases for manipulating RQL in schema #########################################
+
+def guess_rrqlexpr_mainvars(expression):
+    defined = set(split_expression(expression))
+    mainvars = set()
+    if 'S' in defined:
+        mainvars.add('S')
+    if 'O' in defined:
+        mainvars.add('O')
+    if 'U' in defined:
+        mainvars.add('U')
+    if not mainvars:
+        raise Exception('unable to guess selection variables')
+    return mainvars
+
+def split_expression(rqlstring):
+    for expr in rqlstring.split(','):
+        for noparen1 in expr.split('('):
+            for noparen2 in noparen1.split(')'):
+                for word in noparen2.split():
+                    yield word
+
+def normalize_expression(rqlstring):
+    """normalize an rql expression to ease schema synchronization (avoid
+    suppressing and reinserting an expression if only a space has been
+    added/removed for instance)
+    """
+    return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
+
+
+class RQLExpression(object):
+    """Base class for RQL expression used in schema (constraints and
+    permissions)
+    """
+    # these are overridden by set_log_methods below
+    # only defining here to prevent pylint from complaining
+    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+    # to be defined in concrete classes
+    rqlst = None
+    predefined_variables = None
+
+    def __init__(self, expression, mainvars, eid):
+        """
+        :type mainvars: sequence of RQL variables' names. Can be provided as a
+                        comma separated string.
+        :param mainvars: names of the variables being selected.
+
+        """
+        self.eid = eid # eid of the entity representing this rql expression
+        assert mainvars, 'bad mainvars %s' % mainvars
+        if isinstance(mainvars, basestring):
+            mainvars = set(splitstrip(mainvars))
+        elif not isinstance(mainvars, set):
+            mainvars = set(mainvars)
+        self.mainvars = mainvars
+        self.expression = normalize_expression(expression)
+        try:
+            self.full_rql = self.rqlst.as_string()
+        except RQLSyntaxError:
+            raise RQLSyntaxError(expression)
+        for mainvar in mainvars:
+            # if variable is predefined, an extra reference is inserted
+            # automatically (`VAR eid %(v)s`)
+            if mainvar in self.predefined_variables:
+                min_refs = 3
+            else:
+                min_refs = 2
+            if len(self.rqlst.defined_vars[mainvar].references()) < min_refs:
+                _LOGGER.warn('You did not use the %s variable in your RQL '
+                             'expression %s', mainvar, self)
+        # syntax tree used by read security (inserted in queries when necessary)
+        self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
+        # graph of links between variables, used by rql rewriter
+        self.vargraph = vargraph(self.rqlst)
+        # useful for some instrumentation, e.g. localperms permcheck command
+        self.package = ybo.PACKAGE
+
+    def __str__(self):
+        return self.full_rql
+    def __repr__(self):
+        return '%s(%s)' % (self.__class__.__name__, self.full_rql)
+
+    def __lt__(self, other):
+        if hasattr(other, 'expression'):
+            return self.expression < other.expression
+        return True
+
+    def __eq__(self, other):
+        if hasattr(other, 'expression'):
+            return self.expression == other.expression
+        return False
+
+    def __hash__(self):
+        return hash(self.expression)
+
+    def __deepcopy__(self, memo):
+        return self.__class__(self.expression, self.mainvars)
+    def __getstate__(self):
+        return (self.expression, self.mainvars)
+    def __setstate__(self, state):
+        self.__init__(*state)
+
+    @cachedproperty
+    def rqlst(self):
+        select = parse(self.minimal_rql, print_errors=False).children[0]
+        defined = set(split_expression(self.expression))
+        for varname in self.predefined_variables:
+            if varname in defined:
+                select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute')
+        return select
+
+    # permission rql expression specific stuff #################################
+
+    @cached
+    def transform_has_permission(self):
+        found = None
+        rqlst = self.rqlst
+        for var in rqlst.defined_vars.itervalues():
+            for varref in var.references():
+                rel = varref.relation()
+                if rel is None:
+                    continue
+                try:
+                    prefix, action, suffix = rel.r_type.split('_')
+                except ValueError:
+                    continue
+                if prefix != 'has' or suffix != 'permission' or \
+                       not action in ('add', 'delete', 'update', 'read'):
+                    continue
+                if found is None:
+                    found = []
+                    rqlst.save_state()
+                assert rel.children[0].name == 'U'
+                objvar = rel.children[1].children[0].variable
+                rqlst.remove_node(rel)
+                selected = [v.name for v in rqlst.get_selected_variables()]
+                if objvar.name not in selected:
+                    colindex = len(selected)
+                    rqlst.add_selected(objvar)
+                else:
+                    colindex = selected.index(objvar.name)
+                found.append((action, colindex))
+                # remove U eid %(u)s if U is not used in any other relation
+                uvrefs = rqlst.defined_vars['U'].references()
+                if len(uvrefs) == 1:
+                    rqlst.remove_node(uvrefs[0].relation())
+        if found is not None:
+            rql = rqlst.as_string()
+            if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation):
+                # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql
+                keyarg = rqlst.selection[0].name.lower()
+            else:
+                keyarg = None
+            rqlst.recover()
+            return rql, found, keyarg
+        return rqlst.as_string(), None, None
+
+    def _check(self, _cw, **kwargs):
+        """return True if the rql expression is matching the given relation
+        between fromeid and toeid
+
+        _cw may be a request or a server side transaction
+        """
+        creating = kwargs.get('creating')
+        if not creating and self.eid is not None:
+            key = (self.eid, tuple(sorted(kwargs.iteritems())))
+            try:
+                return _cw.local_perm_cache[key]
+            except KeyError:
+                pass
+        rql, has_perm_defs, keyarg = self.transform_has_permission()
+        # when creating an entity, expression related to X satisfied
+        if creating and 'X' in self.rqlst.defined_vars:
+            return True
+        if keyarg is None:
+            kwargs.setdefault('u', _cw.user.eid)
+            try:
+                rset = _cw.execute(rql, kwargs, build_descr=True)
+            except NotImplementedError:
+                self.critical('cant check rql expression, unsupported rql %s', rql)
+                if self.eid is not None:
+                    _cw.local_perm_cache[key] = False
+                return False
+            except TypeResolverException as ex:
+                # some expression may not be resolvable with current kwargs
+                # (type conflict)
+                self.warning('%s: %s', rql, str(ex))
+                if self.eid is not None:
+                    _cw.local_perm_cache[key] = False
+                return False
+            except Unauthorized as ex:
+                self.debug('unauthorized %s: %s', rql, str(ex))
+                if self.eid is not None:
+                    _cw.local_perm_cache[key] = False
+                return False
+        else:
+            rset = _cw.eid_rset(kwargs[keyarg])
+        # if no special has_*_permission relation in the rql expression, just
+        # check the result set contains something
+        if has_perm_defs is None:
+            if rset:
+                if self.eid is not None:
+                    _cw.local_perm_cache[key] = True
+                return True
+        elif rset:
+            # check every special has_*_permission relation is satisfied
+            get_eschema = _cw.vreg.schema.eschema
+            try:
+                for eaction, col in has_perm_defs:
+                    for i in xrange(len(rset)):
+                        eschema = get_eschema(rset.description[i][col])
+                        eschema.check_perm(_cw, eaction, eid=rset[i][col])
+                if self.eid is not None:
+                    _cw.local_perm_cache[key] = True
+                return True
+            except Unauthorized:
+                pass
+        if self.eid is not None:
+            _cw.local_perm_cache[key] = False
+        return False
+
+    @property
+    def minimal_rql(self):
+        return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
+                                    self.expression)
+
+# rql expressions for use in permission definition #############################
+
+class ERQLExpression(RQLExpression):
+    predefined_variables = 'XU'
+
+    def __init__(self, expression, mainvars=None, eid=None):
+        RQLExpression.__init__(self, expression, mainvars or 'X', eid)
+
+    def check(self, _cw, eid=None, creating=False, **kwargs):
+        if 'X' in self.rqlst.defined_vars:
+            if eid is None:
+                if creating:
+                    return self._check(_cw, creating=True, **kwargs)
+                return False
+            assert creating == False
+            return self._check(_cw, x=eid, **kwargs)
+        return self._check(_cw, **kwargs)
+
+
+def vargraph(rqlst):
+    """ builds an adjacency graph of variables from the rql syntax tree, e.g:
+    Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF
+    => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']}
+    """
+    vargraph = {}
+    for relation in rqlst.get_nodes(nodes.Relation):
+        try:
+            rhsvarname = relation.children[1].children[0].variable.name
+            lhsvarname = relation.children[0].name
+        except AttributeError:
+            pass
+        else:
+            vargraph.setdefault(lhsvarname, []).append(rhsvarname)
+            vargraph.setdefault(rhsvarname, []).append(lhsvarname)
+            #vargraph[(lhsvarname, rhsvarname)] = relation.r_type
+    return vargraph
+
+
+class GeneratedConstraint(object):
+    def __init__(self, rqlst, mainvars):
+        self.snippet_rqlst = rqlst
+        self.mainvars = mainvars
+        self.vargraph = vargraph(rqlst)
+
+
+class RRQLExpression(RQLExpression):
+    predefined_variables = 'SOU'
+
+    def __init__(self, expression, mainvars=None, eid=None):
+        if mainvars is None:
+            mainvars = guess_rrqlexpr_mainvars(expression)
+        RQLExpression.__init__(self, expression, mainvars, eid)
+
+    def check(self, _cw, fromeid=None, toeid=None):
+        kwargs = {}
+        if 'S' in self.rqlst.defined_vars:
+            if fromeid is None:
+                return False
+            kwargs['s'] = fromeid
+        if 'O' in self.rqlst.defined_vars:
+            if toeid is None:
+                return False
+            kwargs['o'] = toeid
+        return self._check(_cw, **kwargs)
+
 PUB_SYSTEM_ENTITY_PERMS = {
     'read':   ('managers', 'users', 'guests',),
     'add':    ('managers',),
@@ -659,297 +950,6 @@
     def schema_by_eid(self, eid):
         return self._eid_index[eid]
 
-# Bases for manipulating RQL in schema #########################################
-
-def guess_rrqlexpr_mainvars(expression):
-    defined = set(split_expression(expression))
-    mainvars = set()
-    if 'S' in defined:
-        mainvars.add('S')
-    if 'O' in defined:
-        mainvars.add('O')
-    if 'U' in defined:
-        mainvars.add('U')
-    if not mainvars:
-        raise Exception('unable to guess selection variables')
-    return mainvars
-
-def split_expression(rqlstring):
-    for expr in rqlstring.split(','):
-        for noparen1 in expr.split('('):
-            for noparen2 in noparen1.split(')'):
-                for word in noparen2.split():
-                    yield word
-
-def normalize_expression(rqlstring):
-    """normalize an rql expression to ease schema synchronization (avoid
-    suppressing and reinserting an expression if only a space has been
-    added/removed for instance)
-    """
-    return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
-
-
-class RQLExpression(object):
-    """Base class for RQL expression used in schema (constraints and
-    permissions)
-    """
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-    # to be defined in concrete classes
-    rqlst = None
-    predefined_variables = None
-
-    def __init__(self, expression, mainvars, eid):
-        """
-        :type mainvars: sequence of RQL variables' names. Can be provided as a
-                        comma separated string.
-        :param mainvars: names of the variables being selected.
-
-        """
-        self.eid = eid # eid of the entity representing this rql expression
-        assert mainvars, 'bad mainvars %s' % mainvars
-        if isinstance(mainvars, basestring):
-            mainvars = set(splitstrip(mainvars))
-        elif not isinstance(mainvars, set):
-            mainvars = set(mainvars)
-        self.mainvars = mainvars
-        self.expression = normalize_expression(expression)
-        try:
-            self.full_rql = self.rqlst.as_string()
-        except RQLSyntaxError:
-            raise RQLSyntaxError(expression)
-        for mainvar in mainvars:
-            # if variable is predefined, an extra reference is inserted
-            # automatically (`VAR eid %(v)s`)
-            if mainvar in self.predefined_variables:
-                min_refs = 3
-            else:
-                min_refs = 2
-            if len(self.rqlst.defined_vars[mainvar].references()) < min_refs:
-                _LOGGER.warn('You did not use the %s variable in your RQL '
-                             'expression %s', mainvar, self)
-        # syntax tree used by read security (inserted in queries when necessary)
-        self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
-        # graph of links between variables, used by rql rewriter
-        self.vargraph = vargraph(self.rqlst)
-        # useful for some instrumentation, e.g. localperms permcheck command
-        self.package = ybo.PACKAGE
-
-    def __str__(self):
-        return self.full_rql
-    def __repr__(self):
-        return '%s(%s)' % (self.__class__.__name__, self.full_rql)
-
-    def __lt__(self, other):
-        if hasattr(other, 'expression'):
-            return self.expression < other.expression
-        return True
-
-    def __eq__(self, other):
-        if hasattr(other, 'expression'):
-            return self.expression == other.expression
-        return False
-
-    def __hash__(self):
-        return hash(self.expression)
-
-    def __deepcopy__(self, memo):
-        return self.__class__(self.expression, self.mainvars)
-    def __getstate__(self):
-        return (self.expression, self.mainvars)
-    def __setstate__(self, state):
-        self.__init__(*state)
-
-    @cachedproperty
-    def rqlst(self):
-        select = parse(self.minimal_rql, print_errors=False).children[0]
-        defined = set(split_expression(self.expression))
-        for varname in self.predefined_variables:
-            if varname in defined:
-                select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute')
-        return select
-
-    # permission rql expression specific stuff #################################
-
-    @cached
-    def transform_has_permission(self):
-        found = None
-        rqlst = self.rqlst
-        for var in rqlst.defined_vars.itervalues():
-            for varref in var.references():
-                rel = varref.relation()
-                if rel is None:
-                    continue
-                try:
-                    prefix, action, suffix = rel.r_type.split('_')
-                except ValueError:
-                    continue
-                if prefix != 'has' or suffix != 'permission' or \
-                       not action in ('add', 'delete', 'update', 'read'):
-                    continue
-                if found is None:
-                    found = []
-                    rqlst.save_state()
-                assert rel.children[0].name == 'U'
-                objvar = rel.children[1].children[0].variable
-                rqlst.remove_node(rel)
-                selected = [v.name for v in rqlst.get_selected_variables()]
-                if objvar.name not in selected:
-                    colindex = len(selected)
-                    rqlst.add_selected(objvar)
-                else:
-                    colindex = selected.index(objvar.name)
-                found.append((action, colindex))
-                # remove U eid %(u)s if U is not used in any other relation
-                uvrefs = rqlst.defined_vars['U'].references()
-                if len(uvrefs) == 1:
-                    rqlst.remove_node(uvrefs[0].relation())
-        if found is not None:
-            rql = rqlst.as_string()
-            if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation):
-                # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql
-                keyarg = rqlst.selection[0].name.lower()
-            else:
-                keyarg = None
-            rqlst.recover()
-            return rql, found, keyarg
-        return rqlst.as_string(), None, None
-
-    def _check(self, _cw, **kwargs):
-        """return True if the rql expression is matching the given relation
-        between fromeid and toeid
-
-        _cw may be a request or a server side transaction
-        """
-        creating = kwargs.get('creating')
-        if not creating and self.eid is not None:
-            key = (self.eid, tuple(sorted(kwargs.iteritems())))
-            try:
-                return _cw.local_perm_cache[key]
-            except KeyError:
-                pass
-        rql, has_perm_defs, keyarg = self.transform_has_permission()
-        # when creating an entity, expression related to X satisfied
-        if creating and 'X' in self.rqlst.defined_vars:
-            return True
-        if keyarg is None:
-            kwargs.setdefault('u', _cw.user.eid)
-            try:
-                rset = _cw.execute(rql, kwargs, build_descr=True)
-            except NotImplementedError:
-                self.critical('cant check rql expression, unsupported rql %s', rql)
-                if self.eid is not None:
-                    _cw.local_perm_cache[key] = False
-                return False
-            except TypeResolverException as ex:
-                # some expression may not be resolvable with current kwargs
-                # (type conflict)
-                self.warning('%s: %s', rql, str(ex))
-                if self.eid is not None:
-                    _cw.local_perm_cache[key] = False
-                return False
-            except Unauthorized as ex:
-                self.debug('unauthorized %s: %s', rql, str(ex))
-                if self.eid is not None:
-                    _cw.local_perm_cache[key] = False
-                return False
-        else:
-            rset = _cw.eid_rset(kwargs[keyarg])
-        # if no special has_*_permission relation in the rql expression, just
-        # check the result set contains something
-        if has_perm_defs is None:
-            if rset:
-                if self.eid is not None:
-                    _cw.local_perm_cache[key] = True
-                return True
-        elif rset:
-            # check every special has_*_permission relation is satisfied
-            get_eschema = _cw.vreg.schema.eschema
-            try:
-                for eaction, col in has_perm_defs:
-                    for i in xrange(len(rset)):
-                        eschema = get_eschema(rset.description[i][col])
-                        eschema.check_perm(_cw, eaction, eid=rset[i][col])
-                if self.eid is not None:
-                    _cw.local_perm_cache[key] = True
-                return True
-            except Unauthorized:
-                pass
-        if self.eid is not None:
-            _cw.local_perm_cache[key] = False
-        return False
-
-    @property
-    def minimal_rql(self):
-        return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
-                                    self.expression)
-
-# rql expressions for use in permission definition #############################
-
-class ERQLExpression(RQLExpression):
-    predefined_variables = 'XU'
-
-    def __init__(self, expression, mainvars=None, eid=None):
-        RQLExpression.__init__(self, expression, mainvars or 'X', eid)
-
-    def check(self, _cw, eid=None, creating=False, **kwargs):
-        if 'X' in self.rqlst.defined_vars:
-            if eid is None:
-                if creating:
-                    return self._check(_cw, creating=True, **kwargs)
-                return False
-            assert creating == False
-            return self._check(_cw, x=eid, **kwargs)
-        return self._check(_cw, **kwargs)
-
-
-def vargraph(rqlst):
-    """ builds an adjacency graph of variables from the rql syntax tree, e.g:
-    Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF
-    => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']}
-    """
-    vargraph = {}
-    for relation in rqlst.get_nodes(nodes.Relation):
-        try:
-            rhsvarname = relation.children[1].children[0].variable.name
-            lhsvarname = relation.children[0].name
-        except AttributeError:
-            pass
-        else:
-            vargraph.setdefault(lhsvarname, []).append(rhsvarname)
-            vargraph.setdefault(rhsvarname, []).append(lhsvarname)
-            #vargraph[(lhsvarname, rhsvarname)] = relation.r_type
-    return vargraph
-
-
-class GeneratedConstraint(object):
-    def __init__(self, rqlst, mainvars):
-        self.snippet_rqlst = rqlst
-        self.mainvars = mainvars
-        self.vargraph = vargraph(rqlst)
-
-
-class RRQLExpression(RQLExpression):
-    predefined_variables = 'SOU'
-
-    def __init__(self, expression, mainvars=None, eid=None):
-        if mainvars is None:
-            mainvars = guess_rrqlexpr_mainvars(expression)
-        RQLExpression.__init__(self, expression, mainvars, eid)
-
-    def check(self, _cw, fromeid=None, toeid=None):
-        kwargs = {}
-        if 'S' in self.rqlst.defined_vars:
-            if fromeid is None:
-                return False
-            kwargs['s'] = fromeid
-        if 'O' in self.rqlst.defined_vars:
-            if toeid is None:
-                return False
-            kwargs['o'] = toeid
-        return self._check(_cw, **kwargs)
-
 
 # in yams, default 'update' perm for attributes granted to managers and owners.
 # Within cw, we want to default to users who may edit the entity holding the