"""RQL rewriting utilities, used for read security checking:organization: Logilab:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""fromrqlimportnodes,stmts,TypeResolverExceptionfromcubicwebimportUnauthorized,server,typed_eidfromcubicweb.server.ssplannerimportadd_types_restrictiondefremove_solutions(origsolutions,solutions,defined):"""when a rqlst has been generated from another by introducing security assertions, this method returns solutions which are contained in orig solutions """newsolutions=[]fororigsolinorigsolutions:fornewsolinsolutions[:]:forvar,etypeinorigsol.items():try:ifnewsol[var]!=etype:try:defined[var].stinfo['possibletypes'].remove(newsol[var])exceptKeyError:passbreakexceptKeyError:# variable has been rewrittencontinueelse:newsolutions.append(newsol)solutions.remove(newsol)returnnewsolutionsclassUnsupported(Exception):passclassRQLRewriter(object):"""insert some rql snippets into another rql syntax tree"""def__init__(self,querier,session):self.session=sessionself.annotate=querier._rqlhelper.annotateself._compute_solutions=querier.solutionsself.schema=querier.schemadefcompute_solutions(self):self.annotate(self.select)try:self._compute_solutions(self.session,self.select,self.kwargs)exceptTypeResolverException:raiseUnsupported()iflen(self.select.solutions)<len(self.solutions):raiseUnsupported()defrewrite(self,select,snippets,solutions,kwargs):ifserver.DEBUG:print'---- rewrite',select,snippets,solutionsself.select=selectself.solutions=solutionsself.kwargs=kwargsself.u_varname=Noneself.removing_ambiguity=Falseself.exists_snippet={}# we have to annotate the rqlst before inserting snippets, even though# we'll have to redo it latterself.annotate(select)self.insert_snippets(snippets)ifnotself.exists_snippetandself.u_varname:# U has been inserted than cancelled, cleanupselect.undefine_variable(select.defined_vars[self.u_varname])# clean solutions according to initial solutionsnewsolutions=remove_solutions(solutions,select.solutions,select.defined_vars)assertlen(newsolutions)>=len(solutions), \'rewritten rql %s has lost some solutions, there is probably something '\'wrong in your schema permission (for instance using a '\'RQLExpression which insert a relation which doesn\'t exists in '\'the schema)\nOrig solutions: %s\nnew solutions: %s'%(select,solutions,newsolutions)iflen(newsolutions)>len(solutions):# the snippet has introduced some ambiguities, we have to resolve them# "manually"variantes=self.build_variantes(newsolutions)# insert "is" where necessaryvarexistsmap={}self.removing_ambiguity=Truefor(erqlexpr,mainvar,oldvarname),etypeinvariantes[0].iteritems():varname=self.rewritten[(erqlexpr,mainvar,oldvarname)]var=select.defined_vars[varname]exists=var.references()[0].scopeexists.add_constant_restriction(var,'is',etype,'etype')varexistsmap[mainvar]=exists# insert ORED exists where necessaryforvarianteinvariantes[1:]:self.insert_snippets(snippets,varexistsmap)for(erqlexpr,mainvar,oldvarname),etypeinvariante.iteritems():varname=self.rewritten[(erqlexpr,mainvar,oldvarname)]try:var=select.defined_vars[varname]exceptKeyError:# not a newly inserted variablecontinueexists=var.references()[0].scopeexists.add_constant_restriction(var,'is',etype,'etype')# recompute solutions#select.annotated = False # avoid assertion errorself.compute_solutions()# clean solutions according to initial solutionsnewsolutions=remove_solutions(solutions,select.solutions,select.defined_vars)select.solutions=newsolutionsadd_types_restriction(self.schema,select)ifserver.DEBUG:print'---- rewriten',selectdefbuild_variantes(self,newsolutions):variantes=set()forsolinnewsolutions:variante=[]for(erqlexpr,mainvar,oldvar),newvarinself.rewritten.iteritems():variante.append(((erqlexpr,mainvar,oldvar),sol[newvar]))variantes.add(tuple(variante))# rebuild variantes as dictvariantes=[dict(variante)forvarianteinvariantes]# remove variable which have always the same typeforerqlexpr,mainvar,oldvarinself.rewritten:it=iter(variantes)etype=it.next()[(erqlexpr,mainvar,oldvar)]forvarianteinit:ifvariante[(erqlexpr,mainvar,oldvar)]!=etype:breakelse:forvarianteinvariantes:delvariante[(erqlexpr,mainvar,oldvar)]returnvariantesdefinsert_snippets(self,snippets,varexistsmap=None):self.rewritten={}forvarname,erqlexprsinsnippets:ifvarexistsmapisnotNoneandnotvarnameinvarexistsmap:continuetry:self.const=typed_eid(varname)self.varname=self.constself.rhs_rels=self.lhs_rels={}exceptValueError:self.varname=varnameself.const=Noneself.varstinfo=stinfo=self.select.defined_vars[varname].stinfoifvarexistsmapisNone:self.rhs_rels=dict((rel.r_type,rel)forrelinstinfo['rhsrelations'])self.lhs_rels=dict((rel.r_type,rel)forrelinstinfo['relations']ifnotrelinstinfo['rhsrelations'])else:self.rhs_rels=self.lhs_rels={}parent=Noneinserted=Falseforerqlexprinerqlexprs:self.current_expr=erqlexprifvarexistsmapisNone:try:new=self.insert_snippet(varname,erqlexpr.snippet_rqlst,parent)exceptUnsupported:continueinserted=TrueifnewisnotNone:self.exists_snippet[erqlexpr]=newparent=parentornewelse:# called to reintroduce snippet due to ambiguity creation,# so skip snippets which are not introducing this ambiguityexists=varexistsmap[varname]ifself.exists_snippet[erqlexpr]isexists:self.insert_snippet(varname,erqlexpr.snippet_rqlst,exists)ifvarexistsmapisNoneandnotinserted:# no rql expression found matching rql solutions. User has no access rightraiseUnauthorized()definsert_snippet(self,varname,snippetrqlst,parent=None):new=snippetrqlst.where.accept(self)ifnewisnotNone:try:var=self.select.defined_vars[varname]exceptKeyError:# not a variablepasselse:ifvar.stinfo['optrelations']:# use a subquerysubselect=stmts.Select()subselect.append_selected(nodes.VariableRef(subselect.get_variable(varname)))subselect.add_restriction(new.copy(subselect))aliases=[varname]forrelinvar.stinfo['relations']:rschema=self.schema.rschema(rel.r_type)ifrschema.is_final()or(rschema.inlinedandnotrelinvar.stinfo['rhsrelations']):self.select.remove_node(rel)rel.children[0].name=varnamesubselect.add_restriction(rel.copy(subselect))forvrefinrel.children[1].iget_nodes(nodes.VariableRef):subselect.append_selected(vref.copy(subselect))aliases.append(vref.name)ifself.u_varname:# generate an identifier for the substitutionargname=subselect.allocate_varname()whileargnameinself.kwargs:argname=subselect.allocate_varname()subselect.add_constant_restriction(subselect.get_variable(self.u_varname),'eid',unicode(argname),'Substitute')self.kwargs[argname]=self.session.user.eidadd_types_restriction(self.schema,subselect,subselect,solutions=self.solutions)assertparentisNonemyunion=stmts.Union()myunion.append(subselect)aliases=[nodes.VariableRef(self.select.get_variable(name,i))fori,nameinenumerate(aliases)]self.select.add_subquery(nodes.SubQuery(aliases,myunion),check=False)self._cleanup_inserted(new)try:self.compute_solutions()exceptUnsupported:# some solutions have been lost, can't apply this rql exprself.select.remove_subquery(new,undefine=True)raisereturnnew=nodes.Exists(new)ifparentisNone:self.select.add_restriction(new)else:grandpa=parent.parentor_=nodes.Or(parent,new)grandpa.replace(parent,or_)ifnotself.removing_ambiguity:try:self.compute_solutions()exceptUnsupported:# some solutions have been lost, can't apply this rql exprifparentisNone:self.select.remove_node(new,undefine=True)else:parent.parent.replace(or_,or_.children[0])self._cleanup_inserted(new)raisereturnnewdef_cleanup_inserted(self,node):# cleanup inserted variable referencesforvrefinnode.iget_nodes(nodes.VariableRef):vref.unregister_reference()ifnotvref.variable.stinfo['references']:# no more references, undefine the variabledelself.select.defined_vars[vref.name]def_visit_binary(self,node,cls):newnode=cls()forcinnode.children:new=c.accept(self)ifnewisNone:continuenewnode.append(new)iflen(newnode.children)==0:returnNoneiflen(newnode.children)==1:returnnewnode.children[0]returnnewnodedef_visit_unary(self,node,cls):newc=node.children[0].accept(self)ifnewcisNone:returnNonenewnode=cls()newnode.append(newc)returnnewnodedefvisit_and(self,et):returnself._visit_binary(et,nodes.And)defvisit_or(self,ou):returnself._visit_binary(ou,nodes.Or)defvisit_not(self,node):returnself._visit_unary(node,nodes.Not)defvisit_exists(self,node):returnself._visit_unary(node,nodes.Exists)defvisit_relation(self,relation):lhs,rhs=relation.get_variable_parts()iflhs.name=='X':# on lhs# see if we can reuse this relationifrelation.r_typeinself.lhs_relsandisinstance(rhs,nodes.VariableRef)andrhs.name!='U':ifself._may_be_shared(relation,'object'):# ok, can share variableterm=self.lhs_rels[relation.r_type].children[1].children[0]self._use_outer_term(rhs.name,term)returnelifisinstance(rhs,nodes.VariableRef)andrhs.name=='X'andlhs.name!='U':# on rhs# see if we can reuse this relationifrelation.r_typeinself.rhs_relsandself._may_be_shared(relation,'subject'):# ok, can share variableterm=self.rhs_rels[relation.r_type].children[0]self._use_outer_term(lhs.name,term)returnrel=nodes.Relation(relation.r_type,relation.optional)forcinrelation.children:rel.append(c.accept(self))returnreldefvisit_comparison(self,cmp):cmp_=nodes.Comparison(cmp.operator)forcincmp.children:cmp_.append(c.accept(self))returncmp_defvisit_mathexpression(self,mexpr):cmp_=nodes.MathExpression(mexpr.operator)forcincmp.children:cmp_.append(c.accept(self))returncmp_defvisit_function(self,function):"""generate filter name for a function"""function_=nodes.Function(function.name)forcinfunction.children:function_.append(c.accept(self))returnfunction_defvisit_constant(self,constant):"""generate filter name for a constant"""returnnodes.Constant(constant.value,constant.type)defvisit_variableref(self,vref):"""get the sql name for a variable reference"""ifvref.name=='X':ifself.constisnotNone:returnnodes.Constant(self.const,'Int')returnnodes.VariableRef(self.select.get_variable(self.varname))vname_or_term=self._get_varname_or_term(vref.name)ifisinstance(vname_or_term,basestring):returnnodes.VariableRef(self.select.get_variable(vname_or_term))# shared termreturnvname_or_term.copy(self.select)def_may_be_shared(self,relation,target):"""return True if the snippet relation can be skipped to use a relation from the original query """# if cardinality is in '?1', we can ignore the relation and use variable# from the original queryrschema=self.schema.rschema(relation.r_type)iftarget=='object':cardindex=0ttypes_func=rschema.objectsrprop=rschema.rpropertyelse:# target == 'subject':cardindex=1ttypes_func=rschema.subjectsrprop=lambdax,y,z:rschema.rproperty(y,x,z)foretypeinself.varstinfo['possibletypes']:forttypeinttypes_func(etype):ifrprop(etype,ttype,'cardinality')[cardindex]in'+*':returnFalsereturnTruedef_use_outer_term(self,snippet_varname,term):key=(self.current_expr,self.varname,snippet_varname)ifkeyinself.rewritten:insertedvar=self.select.defined_vars.pop(self.rewritten[key])forinserted_vrefininsertedvar.references():inserted_vref.parent.replace(inserted_vref,term.copy(self.select))self.rewritten[key]=termdef_get_varname_or_term(self,vname):ifvname=='U':ifself.u_varnameisNone:select=self.selectself.u_varname=select.allocate_varname()# generate an identifier for the substitutionargname=select.allocate_varname()whileargnameinself.kwargs:argname=select.allocate_varname()# insert "U eid %(u)s"var=select.get_variable(self.u_varname)select.add_constant_restriction(select.get_variable(self.u_varname),'eid',unicode(argname),'Substitute')self.kwargs[argname]=self.session.user.eidreturnself.u_varnamekey=(self.current_expr,self.varname,vname)try:returnself.rewritten[key]exceptKeyError:self.rewritten[key]=newvname=self.select.allocate_varname()returnnewvname