[server] fix unittest_security for py3k
- use source.binary_to_str instead of str(), helps when the backend
returns memoryviews
- fixup str vs bytes
# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""SQL utilities functions and classes."""from__future__importprint_function__docformat__="restructuredtext en"importsysimportreimportsubprocessfromos.pathimportabspathfromloggingimportgetLoggerfromdatetimeimporttime,datetimefromsiximportstring_types,text_typefromsix.movesimportfilterfromlogilabimportdatabaseasdb,commonaslgcfromlogilab.common.shellutilsimportProgressBar,DummyProgressBarfromlogilab.common.deprecationimportdeprecatedfromlogilab.common.logging_extimportset_log_methodsfromlogilab.common.dateimportutctime,utcdatetimefromlogilab.database.sqlgenimportSQLGeneratorfromcubicwebimportBinary,ConfigurationErrorfromcubicweb.uilibimportremove_html_tagsfromcubicweb.schemaimportPURE_VIRTUAL_RTYPESfromcubicweb.serverimportSQL_CONNECT_HOOKSfromcubicweb.server.utilsimportcrypt_passwordlgc.USE_MX_DATETIME=FalseSQL_PREFIX='cw_'def_run_command(cmd):print(' '.join(cmd))returnsubprocess.call(cmd)defsqlexec(sqlstmts,cursor_or_execute,withpb=True,pbtitle='',delimiter=';',cnx=None):"""execute sql statements ignoring DROP/ CREATE GROUP or USER statements error. :sqlstmts_as_string: a string or a list of sql statements. :cursor_or_execute: sql cursor or a callback used to execute statements :cnx: if given, commit/rollback at each statement. :withpb: if True, display a progresse bar :pbtitle: a string displayed as the progress bar title (if `withpb=True`) :delimiter: a string used to split sqlstmts (if it is a string) Return the failed statements (same type as sqlstmts) """ifhasattr(cursor_or_execute,'execute'):execute=cursor_or_execute.executeelse:execute=cursor_or_executesqlstmts_as_string=Falseifisinstance(sqlstmts,string_types):sqlstmts_as_string=Truesqlstmts=sqlstmts.split(delimiter)ifwithpb:ifsys.stdout.isatty():pb=ProgressBar(len(sqlstmts),title=pbtitle)else:pb=DummyProgressBar()failed=[]forsqlinsqlstmts:sql=sql.strip()ifwithpb:pb.update()ifnotsql:continuetry:# some dbapi modules doesn't accept unicode for sql stringexecute(str(sql))exceptExceptionaserr:ifcnx:cnx.rollback()failed.append(sql)else:ifcnx:cnx.commit()ifwithpb:print()ifsqlstmts_as_string:failed=delimiter.join(failed)returnfaileddefsqlgrants(schema,driver,user,text_index=True,set_owner=True,skip_relations=(),skip_entities=()):"""return sql to give all access privileges to the given user on the system schema """fromcubicweb.server.schema2sqlimportgrant_schemafromcubicweb.server.sourcesimportnativeoutput=[]w=output.appendw(native.grant_schema(user,set_owner))w('')iftext_index:dbhelper=db.get_db_helper(driver)w(dbhelper.sql_grant_user_on_fti(user))w('')w(grant_schema(schema,user,set_owner,skip_entities=skip_entities,prefix=SQL_PREFIX))return'\n'.join(output)defsqlschema(schema,driver,text_index=True,user=None,set_owner=False,skip_relations=PURE_VIRTUAL_RTYPES,skip_entities=()):"""return the system sql schema, according to the given parameters"""fromcubicweb.server.schema2sqlimportschema2sqlfromcubicweb.server.sourcesimportnativeifset_owner:assertuser,'user is argument required when set_owner is true'output=[]w=output.appendw(native.sql_schema(driver))w('')dbhelper=db.get_db_helper(driver)iftext_index:w(dbhelper.sql_init_fti().replace(';',';;'))w('')w(schema2sql(dbhelper,schema,prefix=SQL_PREFIX,skip_entities=skip_entities,skip_relations=skip_relations).replace(';',';;'))ifdbhelper.users_supportanduser:w('')w(sqlgrants(schema,driver,user,text_index,set_owner,skip_relations,skip_entities).replace(';',';;'))return'\n'.join(output)defsqldropschema(schema,driver,text_index=True,skip_relations=PURE_VIRTUAL_RTYPES,skip_entities=()):"""return the sql to drop the schema, according to the given parameters"""fromcubicweb.server.schema2sqlimportdropschema2sqlfromcubicweb.server.sourcesimportnativeoutput=[]w=output.appendiftext_index:dbhelper=db.get_db_helper(driver)w(dbhelper.sql_drop_fti())w('')w(dropschema2sql(dbhelper,schema,prefix=SQL_PREFIX,skip_entities=skip_entities,skip_relations=skip_relations))w('')w(native.sql_drop_schema(driver))return'\n'.join(output)_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION=re.compile('^(?!(sql|pg)_)').matchdefsql_drop_all_user_tables(driver_or_helper,sqlcursor):"""Return ths sql to drop all tables found in the database system."""ifnotgetattr(driver_or_helper,'list_tables',None):dbhelper=db.get_db_helper(driver_or_helper)else:dbhelper=driver_or_helpercmds=[dbhelper.sql_drop_sequence('entities_id_seq')]# for mssql, we need to drop views before tablesifhasattr(dbhelper,'list_views'):cmds+=['DROP VIEW %s;'%namefornameinfilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION,dbhelper.list_views(sqlcursor))]cmds+=['DROP TABLE %s;'%namefornameinfilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION,dbhelper.list_tables(sqlcursor))]return'\n'.join(cmds)classConnectionWrapper(object):"""handle connection to the system source, at some point associated to a :class:`Session` """# since 3.19, we only have to manage the system source connectiondef__init__(self,system_source):# dictionary of (source, connection), indexed by sources'uriself._source=system_sourceself.cnx=system_source.get_connection()self.cu=self.cnx.cursor()defcommit(self):"""commit the current transaction for this user"""# let exception propagatesself.cnx.commit()defrollback(self):"""rollback the current transaction for this user"""# catch exceptions, rollback other sources anywaytry:self.cnx.rollback()exceptException:self._source.critical('rollback error',exc_info=sys.exc_info())# error on rollback, the connection is much probably in a really# bad state. Replace it by a new one.self.reconnect()defclose(self,i_know_what_i_do=False):"""close all connections in the set"""ifi_know_what_i_doisnotTrue:# unexpected closing safety beltraiseRuntimeError('connections set shouldn\'t be closed')try:self.cu.close()self.cu=NoneexceptException:passtry:self.cnx.close()self.cnx=NoneexceptException:pass# internals ###############################################################defcnxset_freed(self):"""connections set is being freed from a session"""pass# no nothing by defaultdefreconnect(self):"""reopen a connection for this source or all sources if none specified """try:# properly close existing connection if anyself.cnx.close()exceptException:passself._source.info('trying to reconnect')self.cnx=self._source.get_connection()self.cu=self.cnx.cursor()@deprecated('[3.19] use .cu instead')def__getitem__(self,uri):asserturi=='system'returnself.cu@deprecated('[3.19] use repo.system_source instead')defsource(self,uid):assertuid=='system'returnself._source@deprecated('[3.19] use .cnx instead')defconnection(self,uid):assertuid=='system'returnself.cnxclassSqliteConnectionWrapper(ConnectionWrapper):"""Sqlite specific connection wrapper: close the connection each time it's freed (and reopen it later when needed) """def__init__(self,system_source):# don't call parent's __init__, we don't want to initiate the connectionself._source=system_source_cnx=Nonedefcnxset_freed(self):self.cu.close()self.cnx.close()self.cnx=self.cu=None@propertydefcnx(self):ifself._cnxisNone:self._cnx=self._source.get_connection()self._cu=self._cnx.cursor()returnself._cnx@cnx.setterdefcnx(self,value):self._cnx=value@propertydefcu(self):ifself._cnxisNone:self._cnx=self._source.get_connection()self._cu=self._cnx.cursor()returnself._cu@cu.setterdefcu(self,value):self._cu=valueclassSQLAdapterMixIn(object):"""Mixin for SQL data sources, getting a connection from a configuration dictionary and handling connection locking """cnx_wrap=ConnectionWrapperdef__init__(self,source_config,repairing=False):try:self.dbdriver=source_config['db-driver'].lower()dbname=source_config['db-name']exceptKeyError:raiseConfigurationError('missing some expected entries in sources file')dbhost=source_config.get('db-host')port=source_config.get('db-port')dbport=portandint(port)orNonedbuser=source_config.get('db-user')dbpassword=source_config.get('db-password')dbencoding=source_config.get('db-encoding','UTF-8')dbextraargs=source_config.get('db-extra-arguments')dbnamespace=source_config.get('db-namespace')self.dbhelper=db.get_db_helper(self.dbdriver)self.dbhelper.record_connection_info(dbname,dbhost,dbport,dbuser,dbpassword,dbextraargs,dbencoding,dbnamespace)self.sqlgen=SQLGenerator()# copy back some commonly accessed attributesdbapi_module=self.dbhelper.dbapi_moduleself.OperationalError=dbapi_module.OperationalErrorself.InterfaceError=dbapi_module.InterfaceErrorself.DbapiError=dbapi_module.Errorself._binary=self.dbhelper.binary_valueself._process_value=dbapi_module.process_valueself._dbencoding=dbencodingifself.dbdriver=='sqlite':self.cnx_wrap=SqliteConnectionWrapperself.dbhelper.dbname=abspath(self.dbhelper.dbname)ifnotrepairing:statement_timeout=int(source_config.get('db-statement-timeout',0))ifstatement_timeout>0:defset_postgres_timeout(cnx):cnx.cursor().execute('SET statement_timeout to %d'%statement_timeout)cnx.commit()postgres_hooks=SQL_CONNECT_HOOKS['postgres']postgres_hooks.append(set_postgres_timeout)defwrapped_connection(self):"""open and return a connection to the database, wrapped into a class handling reconnection and all """returnself.cnx_wrap(self)defget_connection(self):"""open and return a connection to the database"""returnself.dbhelper.get_connection()defbackup_to_file(self,backupfile,confirm):forcmdinself.dbhelper.backup_commands(backupfile,keepownership=False):if_run_command(cmd):ifnotconfirm(' [Failed] Continue anyway?',default='n'):raiseException('Failed command: %s'%cmd)defrestore_from_file(self,backupfile,confirm,drop=True):forcmdinself.dbhelper.restore_commands(backupfile,keepownership=False,drop=drop):if_run_command(cmd):ifnotconfirm(' [Failed] Continue anyway?',default='n'):raiseException('Failed command: %s'%cmd)defmerge_args(self,args,query_args):ifargsisnotNone:newargs={}forkey,valinargs.items():# convert cubicweb binary into db binaryifisinstance(val,Binary):val=self._binary(val.getvalue())# convert timestamp to utc.# expect SET TiME ZONE to UTC at connection opening time.# This shouldn't change anything for datetime without TZ.elifisinstance(val,datetime)andval.tzinfoisnotNone:val=utcdatetime(val)elifisinstance(val,time)andval.tzinfoisnotNone:val=utctime(val)newargs[key]=val# should not collideassertnot(frozenset(newargs)&frozenset(query_args)), \'unexpected collision: %s'%(frozenset(newargs)&frozenset(query_args))newargs.update(query_args)returnnewargsreturnquery_argsdefprocess_result(self,cursor,cnx=None,column_callbacks=None):"""return a list of CubicWeb compliant values from data in the given cursor """returnlist(self.iter_process_result(cursor,cnx,column_callbacks))defiter_process_result(self,cursor,cnx,column_callbacks=None):"""return a iterator on tuples of CubicWeb compliant values from data in the given cursor """# use two different implementations to avoid paying the price of# callback lookup for each *cell* in results when there is nothing to# lookupifnotcolumn_callbacks:returnself.dbhelper.dbapi_module.process_cursor(cursor,self._dbencoding,Binary)assertcnxreturnself._cb_process_result(cursor,column_callbacks,cnx)def_cb_process_result(self,cursor,column_callbacks,cnx):# begin bind to locals for optimizationdescr=cursor.descriptionencoding=self._dbencodingprocess_value=self._process_valuebinary=Binary# /endcursor.arraysize=100whileTrue:results=cursor.fetchmany()ifnotresults:breakforlineinresults:result=[]forcol,valueinenumerate(line):ifvalueisNone:result.append(value)continuecbstack=column_callbacks.get(col,None)ifcbstackisNone:value=process_value(value,descr[col],encoding,binary)else:forcbincbstack:value=cb(self,cnx,value)result.append(value)yieldresultdefpreprocess_entity(self,entity):"""return a dictionary to use as extra argument to cursor.execute to insert/update an entity into a SQL database """attrs={}eschema=entity.e_schemaconverters=getattr(self.dbhelper,'TYPE_CONVERTERS',{})forattr,valueinentity.cw_edited.items():ifvalueisnotNoneandeschema.subjrels[attr].final:atype=str(entity.e_schema.destination(attr))ifatypeinconverters:# It is easier to modify preprocess_entity rather# than add_entity (native) as this behavior# may also be used for update.value=converters[atype](value)elifatype=='Password':# XXX could be done using a TYPE_CONVERTERS callback# if value is a Binary instance, this mean we got it# from a query result and so it is already encryptedifisinstance(value,Binary):value=value.getvalue()else:value=crypt_password(value)value=self._binary(value)elifisinstance(value,Binary):value=self._binary(value.getvalue())attrs[SQL_PREFIX+str(attr)]=valueattrs[SQL_PREFIX+'eid']=entity.eidreturnattrs# these are overridden by set_log_methods below# only defining here to prevent pylint from complaininginfo=warning=error=critical=exception=debug=lambdamsg,*a,**kw:Noneset_log_methods(SQLAdapterMixIn,getLogger('cubicweb.sqladapter'))# connection initialization functions ##########################################definit_sqlite_connexion(cnx):classgroup_concat(object):def__init__(self):self.values=set()defstep(self,value):ifvalueisnotNone:self.values.add(value)deffinalize(self):return', '.join(text_type(v)forvinself.values)cnx.create_aggregate("GROUP_CONCAT",1,group_concat)def_limit_size(text,maxsize,format='text/plain'):iflen(text)<maxsize:returntextifformatin('text/html','text/xhtml','text/xml'):text=remove_html_tags(text)iflen(text)>maxsize:text=text[:maxsize]+'...'returntextdeflimit_size3(text,format,maxsize):return_limit_size(text,maxsize,format)cnx.create_function("LIMIT_SIZE",3,limit_size3)deflimit_size2(text,maxsize):return_limit_size(text,maxsize)cnx.create_function("TEXT_LIMIT_SIZE",2,limit_size2)fromlogilab.common.dateimportstrptimedefweekday(ustr):try:dt=strptime(ustr,'%Y-%m-%d %H:%M:%S')except:dt=strptime(ustr,'%Y-%m-%d')# expect sunday to be 1, saturday 7 while weekday method return 0 for# mondayreturn(dt.weekday()+1)%7cnx.create_function("WEEKDAY",1,weekday)cnx.cursor().execute("pragma foreign_keys = on")importyams.constraintsyams.constraints.patch_sqlite_decimal()sqlite_hooks=SQL_CONNECT_HOOKS.setdefault('sqlite',[])sqlite_hooks.append(init_sqlite_connexion)definit_postgres_connexion(cnx):cnx.cursor().execute('SET TIME ZONE UTC')# commit is needed, else setting are lost if the connection is first# rolled backcnx.commit()postgres_hooks=SQL_CONNECT_HOOKS.setdefault('postgres',[])postgres_hooks.append(init_postgres_connexion)