diff --git a/.ci/travis/linux/blacklist.txt b/.ci/travis/linux/blacklist.txt index 74e0b0401b3..c6157cbd4c9 100755 --- a/.ci/travis/linux/blacklist.txt +++ b/.ci/travis/linux/blacklist.txt @@ -33,3 +33,4 @@ PyQgsServerAccessControl # Need a local postgres installation PyQgsAuthManagerPKIPostgresTest PyQgsAuthManagerPasswordPostgresTest +PyQgsAuthManagerOgrPostgresTest diff --git a/src/auth/basic/qgsauthbasicmethod.cpp b/src/auth/basic/qgsauthbasicmethod.cpp index 11c62541207..af072349a9c 100644 --- a/src/auth/basic/qgsauthbasicmethod.cpp +++ b/src/auth/basic/qgsauthbasicmethod.cpp @@ -42,6 +42,7 @@ QgsAuthBasicMethod::QgsAuthBasicMethod() << QStringLiteral( "wfs" ) // convert to lowercase << QStringLiteral( "wcs" ) << QStringLiteral( "wms" ) + << QStringLiteral( "ogr" ) << QStringLiteral( "proxy" ) ); } @@ -85,7 +86,6 @@ bool QgsAuthBasicMethod::updateNetworkRequest( QNetworkRequest &request, const Q bool QgsAuthBasicMethod::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg, const QString &dataprovider ) { - Q_UNUSED( dataprovider ) QgsAuthMethodConfig mconfig = getMethodConfig( authcfg ); if ( !mconfig.isValid() ) { @@ -102,29 +102,8 @@ bool QgsAuthBasicMethod::updateDataSourceUriItems( QStringList &connectionItems, return false; } - QString userparam = "user='" + escapeUserPass( username ) + '\''; - int userindx = connectionItems.indexOf( QRegExp( "^user='.*" ) ); - if ( userindx != -1 ) - { - connectionItems.replace( userindx, userparam ); - } - else - { - connectionItems.append( userparam ); - } - - QString passparam = "password='" + escapeUserPass( password ) + '\''; - int passindx = connectionItems.indexOf( QRegExp( "^password='.*" ) ); - if ( passindx != -1 ) - { - connectionItems.replace( passindx, passparam ); - } - else - { - connectionItems.append( passparam ); - } - - // add extra CAs + // SSL Extra CAs + QString caparam; QList cas; cas = QgsApplication::authManager()->trustedCaCerts(); // save CAs to temp file @@ -134,18 +113,164 @@ bool QgsAuthBasicMethod::updateDataSourceUriItems( QStringList &connectionItems, QgsAuthCertUtils::certsToPemText( cas ) ); if ( ! caFilePath.isEmpty() ) { - QString caparam = "sslrootcert='" + caFilePath + "'"; - int sslcaindx = connectionItems.indexOf( QRegExp( "^sslrootcert='.*" ) ); - if ( sslcaindx != -1 ) + caparam = "sslrootcert='" + caFilePath + "'"; + } + + // Branch for OGR + if ( dataprovider == QStringLiteral( "ogr" ) ) + { + if ( ! password.isEmpty() ) { - connectionItems.replace( sslcaindx, caparam ); + QString fullUri( connectionItems.first() ); + QString uri( fullUri ); + // Handle sub-layers + if ( fullUri.contains( '|' ) ) + { + uri = uri.left( uri.indexOf( '|' ) ); + } + // At least username must be set... password can be empty + if ( ! username.isEmpty() ) + { + // Inject credentials + if ( uri.startsWith( QStringLiteral( "PG:" ) ) ) + { + bool chopped = false; + if ( uri.endsWith( '"' ) ) + { + uri.chop( 1 ); + chopped = true; + } + if ( !username.isEmpty() ) + { + uri += QStringLiteral( " user='%1'" ).arg( username ); + + if ( !password.isEmpty() ) + uri += QStringLiteral( " password='%1'" ).arg( password ); + } + // add extra CAs + if ( ! caparam.isEmpty() ) + { + uri += ' ' + caparam; + } + if ( chopped ) + uri += '"'; + } + else if ( uri.startsWith( QStringLiteral( "SDE:" ) ) ) + { + uri = uri.replace( QRegExp( ",$" ), QStringLiteral( ",%1,%2" ).arg( username, password ) ); + } + else if ( uri.startsWith( QStringLiteral( "IDB" ) ) ) + { + bool chopped = false; + if ( uri.endsWith( '"' ) ) + { + uri.chop( 1 ); + chopped = true; + } + uri += QStringLiteral( " user=%1" ).arg( username ); + if ( !password.isEmpty() ) + uri += QStringLiteral( " pass=%1" ).arg( password ); + if ( chopped ) + uri += '"'; + } + else if ( uri.startsWith( QStringLiteral( "@driver=ingres" ) ) ) + { + uri += QStringLiteral( ",userid=%1" ).arg( username ); + if ( !password.isEmpty() ) + uri += QStringLiteral( ",password=%1" ).arg( password ); + } + else if ( uri.startsWith( QStringLiteral( "MySQL:" ) ) ) + { + uri += QStringLiteral( ",user=%1" ).arg( username ); + if ( !password.isEmpty() ) + uri += QStringLiteral( ",password=%1" ).arg( password ); + } + else if ( uri.startsWith( QStringLiteral( "MSSQL:" ) ) ) + { + uri += QStringLiteral( ";uid=%1" ).arg( username ); + uri = uri.replace( QLatin1String( ";trusted_connection=yes" ), QString() ); + + if ( !password.isEmpty() ) + uri += QStringLiteral( ";pwd=%1" ).arg( password ); + } + else if ( uri.startsWith( QStringLiteral( "OCI:" ) ) ) + { + // OCI:userid/password@database_instance:table,table + uri = uri.replace( QStringLiteral( "OCI:/" ), QStringLiteral( "OCI:%1/%2" ).arg( username, password ) ); + } + else if ( uri.startsWith( QStringLiteral( "ODBC:" ) ) ) + { + if ( password.isEmpty() ) + { + uri = uri.replace( QRegExp( "^ODBC:@?" ), "ODBC:" + username + '@' ); + } + else + { + uri = uri.replace( QRegExp( "^ODBC:@?" ), "ODBC:" + username + '/' + password + '@' ); + } + } + else if ( uri.startsWith( QStringLiteral( "couchdb" ) ) + || uri.startsWith( QStringLiteral( "DODS" ) ) + || uri.startsWith( "http://" ) + || uri.startsWith( "https://" ) + || uri.startsWith( "ftp://" ) // not really sure that this is supported ... + ) + { + uri = uri.replace( QStringLiteral( "://" ), QStringLiteral( "://%1:%2@" ).arg( username, password ) ); + } + } + // Handle sub-layers + if ( fullUri.contains( '|' ) ) + { + uri += '|' + fullUri.right( fullUri.length() - fullUri.lastIndexOf( '|' ) - 1 ); + } + connectionItems.replace( 0, uri ); } else { - connectionItems.append( caparam ); + QgsDebugMsg( QString( "Update URI items FAILED for authcfg: %1: password empty" ).arg( authcfg ) ); + } + + } + else // Not-ogr + { + QString userparam = "user='" + escapeUserPass( username ) + '\''; + int userindx = connectionItems.indexOf( QRegExp( "^user='.*" ) ); + if ( userindx != -1 ) + { + connectionItems.replace( userindx, userparam ); + } + else + { + connectionItems.append( userparam ); + } + + QString passparam = "password='" + escapeUserPass( password ) + '\''; + int passindx = connectionItems.indexOf( QRegExp( "^password='.*" ) ); + if ( passindx != -1 ) + { + connectionItems.replace( passindx, passparam ); + } + else + { + connectionItems.append( passparam ); + } + // add extra CAs + if ( ! caparam.isEmpty() ) + { + int sslcaindx = connectionItems.indexOf( QRegExp( "^sslrootcert='.*" ) ); + if ( sslcaindx != -1 ) + { + connectionItems.replace( sslcaindx, caparam ); + } + else + { + connectionItems.append( caparam ); + } } } + return true; } diff --git a/src/gui/ogr/qgsnewogrconnection.cpp b/src/gui/ogr/qgsnewogrconnection.cpp index f22be327e31..a5f119f170b 100644 --- a/src/gui/ogr/qgsnewogrconnection.cpp +++ b/src/gui/ogr/qgsnewogrconnection.cpp @@ -57,18 +57,25 @@ QgsNewOgrConnection::QgsNewOgrConnection( QWidget *parent, const QString &connTy txtDatabase->setText( settings.value( key + "/database" ).toString() ); QString port = settings.value( key + "/port" ).toString(); txtPort->setText( port ); - txtUsername->setText( settings.value( key + "/username" ).toString() ); - if ( settings.value( key + "/save" ).toString() == QLatin1String( "true" ) ) + if ( settings.value( key + "/store_username" ).toString() == QLatin1String( "true" ) ) { - txtPassword->setText( settings.value( key + "/password" ).toString() ); - chkStorePassword->setChecked( true ); + mAuthSettingsDatabase->setUsername( settings.value( key + "/username" ).toString() ); + mAuthSettingsDatabase->setStoreUsernameChecked( true ); } + if ( settings.value( key + "/store_password" ).toString() == QLatin1String( "true" ) ) + { + mAuthSettingsDatabase->setPassword( settings.value( key + "/password" ).toString() ); + mAuthSettingsDatabase->setStorePasswordChecked( true ); + } + mAuthSettingsDatabase->setConfigId( settings.value( key + "/configid" ).toString() ); cmbDatabaseTypes->setCurrentIndex( cmbDatabaseTypes->findText( connType ) ); txtName->setText( connName ); txtName->setEnabled( false ); cmbDatabaseTypes->setEnabled( false ); } txtName->setValidator( new QRegExpValidator( QRegExp( "[^\\/]+" ), txtName ) ); + mAuthSettingsDatabase->setDataprovider( QStringLiteral( "ogr" ) ); + mAuthSettingsDatabase->showStoreCheckboxes( true ); } QgsNewOgrConnection::~QgsNewOgrConnection() @@ -80,9 +87,14 @@ QgsNewOgrConnection::~QgsNewOgrConnection() void QgsNewOgrConnection::testConnection() { QString uri; - uri = createDatabaseURI( cmbDatabaseTypes->currentText(), txtHost->text(), - txtDatabase->text(), txtPort->text(), - txtUsername->text(), txtPassword->text() ); + uri = createDatabaseURI( cmbDatabaseTypes->currentText(), + txtHost->text(), + txtDatabase->text(), + txtPort->text(), + mAuthSettingsDatabase->configId(), + mAuthSettingsDatabase->username(), + mAuthSettingsDatabase->password(), + true ); QgsDebugMsg( "Connecting using uri = " + uri ); OGRRegisterAll(); OGRDataSourceH poDS; @@ -133,9 +145,11 @@ void QgsNewOgrConnection::accept() settings.setValue( baseKey + "/host", txtHost->text() ); settings.setValue( baseKey + "/database", txtDatabase->text() ); settings.setValue( baseKey + "/port", txtPort->text() ); - settings.setValue( baseKey + "/username", txtUsername->text() ); - settings.setValue( baseKey + "/password", chkStorePassword->isChecked() ? txtPassword->text() : QLatin1String( "" ) ); - settings.setValue( baseKey + "/save", chkStorePassword->isChecked() ? "true" : "false" ); + settings.setValue( baseKey + "/username", mAuthSettingsDatabase->storeUsernameIsChecked() ? mAuthSettingsDatabase->username() : QLatin1String( "" ) ); + settings.setValue( baseKey + "/password", mAuthSettingsDatabase->storePasswordIsChecked() ? mAuthSettingsDatabase->password() : QLatin1String( "" ) ); + settings.setValue( baseKey + "/store_username", mAuthSettingsDatabase->storeUsernameIsChecked() ? "true" : "false" ); + settings.setValue( baseKey + "/store_password", mAuthSettingsDatabase->storePasswordIsChecked() ? "true" : "false" ); + settings.setValue( baseKey + "/configid", mAuthSettingsDatabase->configId() ); QDialog::accept(); } diff --git a/src/gui/ogr/qgsogrhelperfunctions.cpp b/src/gui/ogr/qgsogrhelperfunctions.cpp index 1b8ae0d14ed..18e74f08105 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.cpp +++ b/src/gui/ogr/qgsogrhelperfunctions.cpp @@ -18,12 +18,24 @@ #include "qgsogrhelperfunctions.h" #include "qgslogger.h" +#include "qgsapplication.h" +#include "qgsauthmanager.h" #include -QString createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &user, const QString &password ) +QString createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig ) { QString uri; + // If an auth configuration is set, override username and password + // Note that only Basic auth (username/password) is for now supported for OGR connections + if ( ! configId.isEmpty() ) + { + // Blank credentials: we are using authcfg! + username = QString(); + password = QString(); + // append authcfg is at the end, because we want to append the authcfg as last argument + } + //todo:add default ports for all kind of databases if ( connectionType == QLatin1String( "ESRI Personal GeoDatabase" ) ) { @@ -34,7 +46,7 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c if ( port.isEmpty() ) port = QStringLiteral( "5151" ); - uri = "SDE:" + host + ",PORT:" + port + ',' + database + ',' + user + ',' + password; + uri = "SDE:" + host + ",PORT:" + port + ',' + database + ',' + username + ',' + password; } else if ( connectionType == QLatin1String( "Informix DataBlade" ) ) { @@ -44,9 +56,9 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c if ( !host.isEmpty() ) uri += QStringLiteral( " server=%1" ).arg( host ); - if ( !user.isEmpty() ) + if ( !username.isEmpty() ) { - uri += QStringLiteral( " user=%1" ).arg( user ); + uri += QStringLiteral( " user=%1" ).arg( username ); if ( !password.isEmpty() ) uri += QStringLiteral( " pass=%1" ).arg( password ); @@ -56,9 +68,9 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c { //not tested uri = "@driver=ingres,dbname=" + database; - if ( !user.isEmpty() ) + if ( !username.isEmpty() ) { - uri += QStringLiteral( ",userid=%1" ).arg( user ); + uri += QStringLiteral( ",userid=%1" ).arg( username ); if ( !password.isEmpty() ) uri += QStringLiteral( ",password=%1" ).arg( password ); @@ -76,9 +88,9 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c uri += QStringLiteral( ",port=%1" ).arg( port ); } - if ( !user.isEmpty() ) + if ( !username.isEmpty() ) { - uri += QStringLiteral( ",user=%1" ).arg( user ); + uri += QStringLiteral( ",user=%1" ).arg( username ); if ( !password.isEmpty() ) uri += QStringLiteral( ",password=%1" ).arg( password ); @@ -96,9 +108,9 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c uri += QStringLiteral( ",%1" ).arg( port ); } - if ( !user.isEmpty() ) + if ( !username.isEmpty() ) { - uri += QStringLiteral( ";uid=%1" ).arg( user ); + uri += QStringLiteral( ";uid=%1" ).arg( username ); if ( !password.isEmpty() ) uri += QStringLiteral( ";pwd=%1" ).arg( password ); @@ -111,10 +123,10 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c } else if ( connectionType == QLatin1String( "Oracle Spatial" ) ) { - uri = "OCI:" + user; + uri = "OCI:" + username; - if ( ( !user.isEmpty() && !password.isEmpty() ) || - ( user.isEmpty() && password.isEmpty() ) ) + if ( ( !username.isEmpty() && !password.isEmpty() ) || + ( username.isEmpty() && password.isEmpty() ) ) { uri += '/'; if ( !password.isEmpty() ) @@ -142,15 +154,15 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c } else if ( connectionType == QLatin1String( "ODBC" ) ) { - if ( !user.isEmpty() ) + if ( !username.isEmpty() ) { if ( password.isEmpty() ) { - uri = "ODBC:" + user + '@' + database; + uri = "ODBC:" + username + '@' + database; } else { - uri = "ODBC:" + user + '/' + password + '@' + database; + uri = "ODBC:" + username + '/' + password + '@' + database; } } @@ -174,9 +186,9 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c uri += QStringLiteral( " port='%1'" ).arg( port ); } - if ( !user.isEmpty() ) + if ( !username.isEmpty() ) { - uri += QStringLiteral( " user='%1'" ).arg( user ); + uri += QStringLiteral( " user='%1'" ).arg( username ); if ( !password.isEmpty() ) uri += QStringLiteral( " password='%1'" ).arg( password ); @@ -184,13 +196,29 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c uri += ' '; } - + // Append authentication configuration to the URI + if ( !( configId.isEmpty() ) ) + { + if ( ! expandAuthConfig ) + { + uri += QStringLiteral( " authcfg='%1'" ).arg( configId ); + } + else + { + QStringList connectionItems; + connectionItems << uri; + if ( QgsApplication::authManager()->updateDataSourceUriItems( connectionItems, configId, QStringLiteral( "ogr" ) ) ) + { + uri = connectionItems.join( QString() ); + } + } + } QgsDebugMsg( "Connection type is=" + connectionType + " and uri=" + uri ); return uri; } -QString createProtocolURI( const QString &type, const QString &url ) +QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig ) { QString uri; if ( type == QLatin1String( "GeoJSON" ) ) @@ -206,5 +234,26 @@ QString createProtocolURI( const QString &type, const QString &url ) uri = QStringLiteral( "DODS:%1" ).arg( url ); } QgsDebugMsg( "Connection type is=" + type + " and uri=" + uri ); + // Update URI with authentication information + if ( ! configId.isEmpty() ) + { + if ( expandAuthConfig ) + { + QStringList connectionItems; + connectionItems << uri; + if ( QgsApplication::authManager()->updateDataSourceUriItems( connectionItems, configId, QStringLiteral( "ogr" ) ) ) + { + uri = connectionItems.join( QString() ); + } + } + else + { + uri += QStringLiteral( " authcfg='%1'" ).arg( configId ); + } + } + else if ( !( username.isEmpty() || password.isEmpty( ) ) ) + { + uri.replace( QStringLiteral( "://" ), QStringLiteral( "://%1:%2@" ).arg( username, password ) ); + } return uri; } diff --git a/src/gui/ogr/qgsogrhelperfunctions.h b/src/gui/ogr/qgsogrhelperfunctions.h index 08d7da10762..5c58bf6d0b9 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.h +++ b/src/gui/ogr/qgsogrhelperfunctions.h @@ -26,11 +26,11 @@ * \brief Create database uri from connection parameters * \note not available in python bindings */ -QString GUI_EXPORT createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &user, const QString &password ); +QString GUI_EXPORT createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig = false ); /** * CreateProtocolURI * \brief Create protocol uri from connection parameters * \note not available in python bindings */ -QString GUI_EXPORT createProtocolURI( const QString &type, const QString &url ); +QString GUI_EXPORT createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig = false ); diff --git a/src/providers/ogr/qgsogrfeatureiterator.cpp b/src/providers/ogr/qgsogrfeatureiterator.cpp index 7166920c7d3..67d9b4467f3 100644 --- a/src/providers/ogr/qgsogrfeatureiterator.cpp +++ b/src/providers/ogr/qgsogrfeatureiterator.cpp @@ -422,7 +422,7 @@ bool QgsOgrFeatureIterator::readFeature( gdal::ogr_feature_unique_ptr fet, QgsFe QgsOgrFeatureSource::QgsOgrFeatureSource( const QgsOgrProvider *p ) - : mDataSource( p->dataSourceUri() ) + : mDataSource( p->dataSourceUri( true ) ) , mLayerName( p->layerName() ) , mLayerIndex( p->layerIndex() ) , mSubsetString( p->mSubsetString ) diff --git a/src/providers/ogr/qgsogrprovider.cpp b/src/providers/ogr/qgsogrprovider.cpp index b1a17720cc4..c2d18ca4076 100644 --- a/src/providers/ogr/qgsogrprovider.cpp +++ b/src/providers/ogr/qgsogrprovider.cpp @@ -469,21 +469,33 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri ) setNativeTypes( nativeTypes ); - QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); } QgsOgrProvider::~QgsOgrProvider() { - QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); // We must also make sure to flush unusef cached connections so that // the file can be removed (#15137) - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); // Do that as last step for final cleanup that might be prevented by // still opened datasets. close(); } +QString QgsOgrProvider::dataSourceUri( bool expandAuthConfig ) const +{ + if ( expandAuthConfig && QgsDataProvider::dataSourceUri( ).contains( QLatin1String( "authcfg" ) ) ) + { + return QgsOgrProviderUtils::expandAuthConfig( QgsDataProvider::dataSourceUri( ) ); + } + else + { + return QgsDataProvider::dataSourceUri( ); + } +} + QgsAbstractFeatureSource *QgsOgrProvider::featureSource() const { return new QgsOgrFeatureSource( this ); @@ -549,9 +561,9 @@ bool QgsOgrProvider::setSubsetString( const QString &theSQL, bool updateFeatureC if ( uri != dataSourceUri() ) { - QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); setDataSourceUri( uri ); - QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); } mOgrLayer->ResetReading(); @@ -901,7 +913,7 @@ OGRwkbGeometryType QgsOgrProvider::getOgrGeomType( OGRLayerH ogrLayer ) void QgsOgrProvider::loadFields() { - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); //the attribute fields need to be read again when the encoding changes mAttributeFields.clear(); mDefaultValues.clear(); @@ -1482,7 +1494,7 @@ bool QgsOgrProvider::addAttributes( const QList &attributes ) { // adding attributes in mapinfo requires to be able to delete the .dat file // so drop any cached connections. - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); } bool returnvalue = true; @@ -1825,7 +1837,7 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_ { pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) ); } - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); return true; } @@ -1901,7 +1913,7 @@ bool QgsOgrProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) commitTransaction(); } - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); return syncToDisc(); } @@ -3294,7 +3306,7 @@ QByteArray QgsOgrProvider::quotedIdentifier( const QByteArray &field ) const void QgsOgrProvider::forceReload() { - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); } QString QgsOgrProviderUtils::connectionPoolId( const QString &dataSourceURI ) @@ -3576,7 +3588,7 @@ QString QgsOgrProviderUtils::quotedValue( const QVariant &value ) bool QgsOgrProvider::syncToDisc() { //for shapefiles, remove spatial index files and create a new index - QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); bool shapeIndex = false; if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { @@ -3591,7 +3603,7 @@ bool QgsOgrProvider::syncToDisc() { shapeIndex = true; close(); - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); QFile::remove( sbnIndexFile ); open( OpenModeSameAsCurrent ); if ( !mValid ) @@ -3615,7 +3627,7 @@ bool QgsOgrProvider::syncToDisc() } #endif - QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); if ( shapeIndex ) { return createSpatialIndex(); @@ -3677,7 +3689,7 @@ void QgsOgrProvider::recalculateFeatureCount() mOgrLayer->SetSpatialFilter( filter ); } - QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri() ) ); + QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) ); } bool QgsOgrProvider::doesStrictFeatureTypeCheck() const @@ -4091,7 +4103,7 @@ static GDALDatasetH OpenHelper( const QString &dsName, option.toUtf8().constData() ); } GDALDatasetH hDS = QgsOgrProviderUtils::GDALOpenWrapper( - dsName.toUtf8().constData(), updateMode, papszOpenOptions, nullptr ); + QgsOgrProviderUtils::expandAuthConfig( dsName ).toUtf8().constData(), updateMode, papszOpenOptions, nullptr ); CSLDestroy( papszOpenOptions ); return hDS; } @@ -4241,6 +4253,7 @@ QgsOgrLayer *QgsOgrProviderUtils::getLayer( const QString &dsName, QString &errCause ) { QMutexLocker locker( &globalMutex ); + for ( auto iter = mapSharedDS.begin(); iter != mapSharedDS.end(); ++iter ) { if ( iter.key().dsName == dsName ) @@ -4290,6 +4303,26 @@ static QDateTime getLastModified( const QString &dsName ) return QFileInfo( dsName ).lastModified(); } +QString QgsOgrProviderUtils::expandAuthConfig( const QString &dsName ) +{ + QString uri( dsName ); + // Check for authcfg + QRegularExpression authcfgRe( " authcfg='([^']+)'" ); + QRegularExpressionMatch match; + if ( uri.contains( authcfgRe, &match ) ) + { + uri = uri.replace( match.captured( 0 ), QString() ); + QString configId( match.captured( 1 ) ); + QStringList connectionItems; + connectionItems << uri; + if ( QgsApplication::authManager()->updateDataSourceUriItems( connectionItems, configId, QStringLiteral( "ogr" ) ) ) + { + uri = connectionItems.first( ); + } + } + return uri; +} + // Must be called under the globalMutex bool QgsOgrProviderUtils::canUseOpenedDatasets( const QString &dsName ) { diff --git a/src/providers/ogr/qgsogrprovider.h b/src/providers/ogr/qgsogrprovider.h index 8768fccd554..cf9fbd7aa2d 100644 --- a/src/providers/ogr/qgsogrprovider.h +++ b/src/providers/ogr/qgsogrprovider.h @@ -64,6 +64,18 @@ class QgsOgrProvider : public QgsVectorDataProvider virtual ~QgsOgrProvider(); + /** + * Get the data source specification. This may be a path or database + * connection string + * \param expandAuthConfig Whether to expand any assigned authentication configuration + * \returns data source specification + * \note The default authentication configuration expansion is FALSE. This keeps credentials + * out of layer data source URIs and project files. Expansion should be specifically done + * only when needed within a provider + */ + QString dataSourceUri( bool expandAuthConfig = false ) const override; + + virtual QgsAbstractFeatureSource *featureSource() const override; virtual QgsCoordinateReferenceSystem crs() const override; @@ -325,6 +337,10 @@ class QgsOgrProviderUtils static bool canUseOpenedDatasets( const QString &dsName ); public: + + //! Inject credentials into the dsName (if any) + static QString expandAuthConfig( const QString &dsName ); + static void setRelevantFields( OGRLayerH ogrLayer, int fieldCount, bool fetchGeometry, const QgsAttributeList &fetchAttributes, bool firstAttrIsFid ); static OGRLayerH setSubsetString( OGRLayerH layer, GDALDatasetH ds, QTextCodec *encoding, const QString &subsetString, bool &origFidAdded ); static QByteArray quotedIdentifier( QByteArray field, const QString &driverName ); diff --git a/src/providers/ogr/qgsogrsourceselect.cpp b/src/providers/ogr/qgsogrsourceselect.cpp index af423225a83..3ae35c781db 100644 --- a/src/providers/ogr/qgsogrsourceselect.cpp +++ b/src/providers/ogr/qgsogrsourceselect.cpp @@ -123,6 +123,8 @@ QgsOgrSourceSelect::QgsOgrSourceSelect( QWidget *parent, Qt::WindowFlags fl, Qgs if ( radioSrcProtocol->isChecked() ) emit enableButtons( !text.isEmpty() ); } ); + // Set filter for ogr compatible auth methods + mAuthSettingsProtocol->setDataprovider( QStringLiteral( "ogr" ) ); } QgsOgrSourceSelect::~QgsOgrSourceSelect() @@ -179,6 +181,7 @@ void QgsOgrSourceSelect::deleteConnection() settings.remove( key + "/password" ); settings.remove( key + "/port" ); settings.remove( key + "/save" ); + settings.remove( key + "/autchcfg" ); settings.remove( key ); cmbConnections->removeItem( cmbConnections->currentIndex() ); // populateConnectionList(); setConnectionListPosition(); @@ -285,9 +288,10 @@ void QgsOgrSourceSelect::addButtonClicked() QString port = settings.value( baseKey + "/port" ).toString(); QString user = settings.value( baseKey + "/username" ).toString(); QString pass = settings.value( baseKey + "/password" ).toString(); + QString configid = settings.value( baseKey + "/configid" ).toString(); bool makeConnection = false; - if ( pass.isEmpty() ) + if ( pass.isEmpty() && configid.isEmpty( ) ) { if ( cmbDatabaseTypes->currentText() == QLatin1String( "MSSQL" ) ) makeConnection = true; @@ -299,13 +303,14 @@ void QgsOgrSourceSelect::addButtonClicked() &makeConnection ); } - if ( makeConnection || !pass.isEmpty() ) + if ( makeConnection || !( pass.isEmpty() && configid.isEmpty( ) ) ) { mDataSources << createDatabaseURI( cmbDatabaseTypes->currentText(), host, database, port, + configid, user, pass ); @@ -321,7 +326,11 @@ void QgsOgrSourceSelect::addButtonClicked() return; } - mDataSources << createProtocolURI( cmbProtocolTypes->currentText(), protocolURI->text() ); + mDataSources << createProtocolURI( cmbProtocolTypes->currentText(), + protocolURI->text(), + mAuthSettingsProtocol->configId(), + mAuthSettingsProtocol->username(), + mAuthSettingsProtocol->password() ); } else if ( radioSrcFile->isChecked() ) { diff --git a/src/ui/qgsnewogrconnectionbase.ui b/src/ui/qgsnewogrconnectionbase.ui index 3d610ba618e..9db282cebbf 100644 --- a/src/ui/qgsnewogrconnectionbase.ui +++ b/src/ui/qgsnewogrconnectionbase.ui @@ -7,7 +7,7 @@ 0 0 404 - 348 + 386 @@ -25,7 +25,7 @@ true - + 9 @@ -41,42 +41,77 @@ 6 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok + + + Connection Information - + + + + + &Test Connection + + + + + + + Authentication + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + + + + - Type + &Type cmbDatabaseTypes - - - - Name + &Name txtName - - - - Name of the new connection - - - @@ -87,22 +122,16 @@ - - - - Database + &Database txtDatabase - - - @@ -113,87 +142,48 @@ - + + + + Name of the new connection + + + + + + + + + + + + + - - - - Username - - - txtUsername - - - - - - - - - - Password - - - txtPassword - - - - - - - QLineEdit::Password - - - - - - - Save Password - - - - - - - &Test Connect - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok - - - - QgsPasswordLineEdit - QLineEdit -
qgspasswordlineedit.h
+ QgsAuthSettingsWidget + QWidget +
auth/qgsauthsettingswidget.h
+ 1
- cmbDatabaseTypes txtName txtHost txtDatabase txtPort - txtUsername - txtPassword - chkStorePassword - btnConnect buttonBox diff --git a/src/ui/qgsogrsourceselectbase.ui b/src/ui/qgsogrsourceselectbase.ui index e943cc8d43f..765892193c3 100644 --- a/src/ui/qgsogrsourceselectbase.ui +++ b/src/ui/qgsogrsourceselectbase.ui @@ -10,7 +10,7 @@ 0 0 450 - 575 + 658
@@ -26,15 +26,76 @@ .. - - - - - QDialogButtonBox::Help + + + + + + 0 + 0 + + + Source type + + + + + + + + F&ile + + + + + + + &Directory + + + + + + + Da&tabase + + + + + + + Protoco&l + + + + + + + + + + + Encoding + + + + + + + + 341 + 0 + + + + + + + - + @@ -52,19 +113,19 @@ Protocol - - - - URI + &URI protocolURI + + + @@ -75,10 +136,41 @@ + + + + Authentication + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + + + + - + @@ -124,12 +216,12 @@ - + - + @@ -206,75 +298,7 @@ - - - - - 0 - 0 - - - - Source type - - - - - - - - File - - - - - - - Directory - - - - - - - Database - - - - - - - Protocol - - - - - - - - - - - Encoding - - - - - - - - 341 - 0 - - - - - - - - - - + Qt::Vertical @@ -287,6 +311,13 @@ + + + + QDialogButtonBox::Help + + + @@ -296,6 +327,12 @@ QWidget
qgsfilewidget.h
+ + QgsAuthSettingsWidget + QWidget +
auth/qgsauthsettingswidget.h
+ 1 +
radioSrcFile @@ -316,38 +353,6 @@ - - buttonBox - accepted() - QgsOgrSourceSelectDialogBase - accept() - - - 451 - 699 - - - 481 - 297 - - - - - buttonBox - rejected() - QgsOgrSourceSelectDialogBase - reject() - - - 392 - 699 - - - 281 - 339 - - - radioSrcDatabase toggled(bool) diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 43c245ef990..4bd048750d5 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -196,6 +196,7 @@ ADD_PYTHON_TEST(PyQgsSourceSelectProvider test_qgssourceselectprovider.py) ADD_PYTHON_TEST(PyQgsAuthManagerProxy test_authmanager_proxy.py) ADD_PYTHON_TEST(PyQgsAuthSettingsWidget test_authsettingswidget.py) ADD_PYTHON_TEST(PyQgsAuxiliaryStorage test_qgsauxiliarystorage.py) +ADD_PYTHON_TEST(PyQgsAuthManagerOgr test_authmanager_ogr.py) IF (NOT WIN32) ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py) @@ -216,7 +217,8 @@ IF (ENABLE_PGTEST) ADD_PYTHON_TEST(PyQgsVectorLayerTools test_qgsvectorlayertools.py) ADD_PYTHON_TEST(PyQgsAuthManagerPKIPostgresTest test_authmanager_pki_postgres.py) ADD_PYTHON_TEST(PyQgsAuthManagerPasswordPostgresTest test_authmanager_password_postgres.py) - ENDIF (ENABLE_PGTEST) + ADD_PYTHON_TEST(PyQgsAuthManagerOgrPostgresTest test_authmanager_ogr_postgres.py) +ENDIF (ENABLE_PGTEST) IF (ENABLE_MSSQLTEST) ADD_PYTHON_TEST(PyQgsMssqlProvider test_provider_mssql.py) diff --git a/tests/src/python/qgis_wrapped_server.py b/tests/src/python/qgis_wrapped_server.py index dc936ef3e62..94bd50c017a 100644 --- a/tests/src/python/qgis_wrapped_server.py +++ b/tests/src/python/qgis_wrapped_server.py @@ -90,6 +90,16 @@ if os.environ.get('QGIS_SERVER_HTTP_BASIC_AUTH') is not None: class HTTPBasicFilter(QgsServerFilter): + def requestReady(self): + handler = self.serverInterface().requestHandler() + auth = self.serverInterface().requestHandler().requestHeader('HTTP_AUTHORIZATION') + if auth: + username, password = base64.b64decode(auth[6:]).split(b':') + if (username.decode('utf-8') == os.environ.get('QGIS_SERVER_USERNAME', 'username') and + password.decode('utf-8') == os.environ.get('QGIS_SERVER_PASSWORD', 'password')): + return + handler.setParameter('SERVICE', 'ACCESS_DENIED') + def responseComplete(self): handler = self.serverInterface().requestHandler() auth = self.serverInterface().requestHandler().requestHeader('HTTP_AUTHORIZATION') diff --git a/tests/src/python/test_authmanager_ogr.py b/tests/src/python/test_authmanager_ogr.py new file mode 100644 index 00000000000..436e8fa1084 --- /dev/null +++ b/tests/src/python/test_authmanager_ogr.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +""" +Tests for auth manager Basic Auth OGR connection credentials injection + +From build dir, run: ctest -R PyQgsAuthManagerOgr -V + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + + +from qgis.core import ( + QgsApplication, + QgsAuthManager, + QgsAuthMethodConfig, + QgsDataSourceUri, + QgsProviderRegistry, +) + +from qgis.testing import ( + start_app, + unittest, +) + + +__author__ = 'Alessandro Pasotti' +__date__ = '14/11/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + + +qgis_app = start_app() + +# Note: value is checked with "in" because some drivers may need additional arguments, +# like temporary paths with rootcerts for PG +TEST_URIS = { + "http://mysite.com/geojson authcfg='%s'": "http://username:password@mysite.com/geojson", + "PG:\"dbname='databasename' host='addr' port='5432' authcfg='%s'\"": "PG:\"dbname='databasename' host='addr' port='5432' user='username' password='password'", + 'SDE:127.0.0.1,12345,dbname, authcfg=\'%s\'': 'SDE:127.0.0.1,12345,dbname,username,password', + 'IDB:"server=demo_on user=informix dbname=frames authcfg=\'%s\'"': 'IDB:"server=demo_on user=informix dbname=frames user=username pass=password"', + '@driver=ingres,dbname=test,tables=usa/canada authcfg=\'%s\'': '@driver=ingres,dbname=test,tables=usa/canada,userid=username,password=password', + 'MySQL:westholland,port=3306,tables=bedrijven authcfg=\'%s\'': 'MySQL:westholland,port=3306,tables=bedrijven,user=username,password=password', + 'MSSQL:server=.\MSSQLSERVER2008;database=dbname;trusted_connection=yes authcfg=\'%s\'': 'MSSQL:server=.\MSSQLSERVER2008;database=dbname;uid=username;pwd=password', + 'OCI:/@database_instance:table,table authcfg=\'%s\'': 'OCI:username/password@database_instance:table,table', + 'ODBC:database_instance authcfg=\'%s\'': 'ODBC:username/password@database_instance', + 'couchdb://myconnection authcfg=\'%s\'': 'couchdb://username:password@myconnection', + 'http://www.myconnection.com/geojson authcfg=\'%s\'': 'http://username:password@www.myconnection.com/geojson', + 'https://www.myconnection.com/geojson authcfg=\'%s\'': 'https://username:password@www.myconnection.com/geojson', + 'ftp://www.myconnection.com/geojson authcfg=\'%s\'': 'ftp://username:password@www.myconnection.com/geojson', + 'DODS://www.myconnection.com/geojson authcfg=\'%s\'': 'DODS://username:password@www.myconnection.com/geojson', +} + + +class TestAuthManager(unittest.TestCase): + + @classmethod + def setUpAuth(cls): + """Run before all tests and set up authentication""" + authm = QgsApplication.authManager() + assert (authm.setMasterPassword('masterpassword', True)) + # Client side + cls.auth_config = QgsAuthMethodConfig("Basic") + cls.auth_config.setConfig('username', cls.username) + cls.auth_config.setConfig('password', cls.password) + cls.auth_config.setName('test_basic_auth_config') + assert (authm.storeAuthenticationConfig(cls.auth_config)[0]) + assert cls.auth_config.isValid() + cls.authcfg = cls.auth_config.id() + + @classmethod + def setUpClass(cls): + """Run before all tests: + Creates an auth configuration""" + cls.username = 'username' + cls.password = 'password' + cls.dbname = 'test_basic' + cls.hostname = 'localhost' + cls.setUpAuth() + + @classmethod + def tearDownClass(cls): + """Run after all tests""" + pass + + def setUp(self): + """Run before each test.""" + pass + + def tearDown(self): + """Run after each test.""" + pass + + def testConnections(self): + """ + Test credentials injection + """ + pr = QgsProviderRegistry.instance().createProvider('ogr', '') + for uri, expanded in TEST_URIS.items(): + pr.setDataSourceUri(uri % self.authcfg) + self.assertTrue(expanded in pr.dataSourceUri(True), "%s != %s" % (expanded, pr.dataSourceUri(True))) + + # Test sublayers + for uri, expanded in TEST_URIS.items(): + pr.setDataSourceUri((uri + '|sublayer1') % self.authcfg) + self.assertEqual(pr.dataSourceUri(True).split('|')[1], "sublayer1", pr.dataSourceUri(True)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_authmanager_ogr_postgres.py b/tests/src/python/test_authmanager_ogr_postgres.py new file mode 100644 index 00000000000..a8a1c32d026 --- /dev/null +++ b/tests/src/python/test_authmanager_ogr_postgres.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +""" +Tests for auth manager Basic Auth access to postgres. + +This is an integration test for QGIS Desktop Auth Manager postgres provider that +checks if QGIS can use a stored auth manager auth configuration to access +a username/password protected postgres. + +Configuration from the environment: + + * QGIS_POSTGRES_SERVER_PORT (default: 55432) + * QGIS_POSTGRES_EXECUTABLE_PATH (default: /usr/lib/postgresql/9.4/bin) + + +From build dir, run: ctest -R PyQgsAuthManagerOgrPostgresTest -V + +or, if your PostgreSQL path differs from the default: + +QGIS_POSTGRES_EXECUTABLE_PATH=/usr/lib/postgresql//bin \ + ctest -R PyQgsAuthManagerOgrPostgresTest -V + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +import os +import time +import signal +import stat +import subprocess +import tempfile + +from shutil import rmtree + +from utilities import unitTestDataPath +from qgis.core import ( + QgsApplication, + QgsAuthManager, + QgsAuthMethodConfig, + QgsVectorLayer, + QgsDataSourceUri, + QgsWkbTypes, +) + +from qgis.PyQt.QtNetwork import QSslCertificate + +from qgis.testing import ( + start_app, + unittest, +) + + +__author__ = 'Alessandro Pasotti' +__date__ = '03/11/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +QGIS_POSTGRES_SERVER_PORT = os.environ.get('QGIS_POSTGRES_SERVER_PORT', '55432') +QGIS_POSTGRES_EXECUTABLE_PATH = os.environ.get('QGIS_POSTGRES_EXECUTABLE_PATH', '/usr/lib/postgresql/9.4/bin') + +assert os.path.exists(QGIS_POSTGRES_EXECUTABLE_PATH) + +QGIS_AUTH_DB_DIR_PATH = tempfile.mkdtemp() + +# Postgres test path +QGIS_PG_TEST_PATH = tempfile.mkdtemp() + +os.environ['QGIS_AUTH_DB_DIR_PATH'] = QGIS_AUTH_DB_DIR_PATH + +qgis_app = start_app() + +QGIS_POSTGRES_CONF_TEMPLATE = """ +hba_file = '%(tempfolder)s/pg_hba.conf' +listen_addresses = '*' +port = %(port)s +max_connections = 100 +unix_socket_directories = '%(tempfolder)s' +ssl = true +ssl_ciphers = 'DEFAULT:!LOW:!EXP:!MD5:@STRENGTH' # allowed SSL ciphers +ssl_cert_file = '%(server_cert)s' +ssl_key_file = '%(server_key)s' +ssl_ca_file = '%(sslrootcert_path)s' +password_encryption = on +""" + +QGIS_POSTGRES_HBA_TEMPLATE = """ +hostssl all all 0.0.0.0/0 md5 +hostssl all all ::1/0 md5 +host all all 127.0.0.1/32 trust +host all all ::1/32 trust + +host all all 0.0.0.0/0 trust + +""" + + +class TestAuthManager(unittest.TestCase): + + @classmethod + def setUpAuth(cls): + """Run before all tests and set up authentication""" + authm = QgsApplication.authManager() + assert (authm.setMasterPassword('masterpassword', True)) + cls.pg_conf = os.path.join(cls.tempfolder, 'postgresql.conf') + cls.pg_hba = os.path.join(cls.tempfolder, 'pg_hba.conf') + # Client side + cls.sslrootcert_path = os.path.join(cls.certsdata_path, 'chains_subissuer-issuer-root_issuer2-root2.pem') + assert os.path.isfile(cls.sslrootcert_path) + os.chmod(cls.sslrootcert_path, stat.S_IRUSR) + cls.auth_config = QgsAuthMethodConfig("Basic") + cls.auth_config.setConfig('username', cls.username) + cls.auth_config.setConfig('password', cls.password) + cls.auth_config.setName('test_basic_auth_config') + cls.sslrootcert = QSslCertificate.fromPath(cls.sslrootcert_path) + assert cls.sslrootcert is not None + authm.storeCertAuthorities(cls.sslrootcert) + authm.rebuildCaCertsCache() + authm.rebuildTrustedCaCertsCache() + authm.rebuildCertTrustCache() + assert (authm.storeAuthenticationConfig(cls.auth_config)[0]) + assert cls.auth_config.isValid() + cls.authcfg = cls.auth_config.id() + + # Server side + cls.server_cert = os.path.join(cls.certsdata_path, 'localhost_ssl_cert.pem') + cls.server_key = os.path.join(cls.certsdata_path, 'localhost_ssl_key.pem') + cls.server_rootcert = cls.sslrootcert_path + os.chmod(cls.server_cert, stat.S_IRUSR) + os.chmod(cls.server_key, stat.S_IRUSR) + os.chmod(cls.server_rootcert, stat.S_IRUSR) + + # Place conf in the data folder + with open(cls.pg_conf, 'w+') as f: + f.write(QGIS_POSTGRES_CONF_TEMPLATE % { + 'port': cls.port, + 'tempfolder': cls.tempfolder, + 'server_cert': cls.server_cert, + 'server_key': cls.server_key, + 'sslrootcert_path': cls.sslrootcert_path, + }) + + with open(cls.pg_hba, 'w+') as f: + f.write(QGIS_POSTGRES_HBA_TEMPLATE) + + @classmethod + def setUpClass(cls): + """Run before all tests: + Creates an auth configuration""" + cls.port = QGIS_POSTGRES_SERVER_PORT + cls.username = 'username' + cls.password = 'password' + cls.dbname = 'test_basic' + cls.tempfolder = QGIS_PG_TEST_PATH + cls.certsdata_path = os.path.join(unitTestDataPath('auth_system'), 'certs_keys') + cls.hostname = 'localhost' + cls.data_path = os.path.join(cls.tempfolder, 'data') + os.mkdir(cls.data_path) + + cls.setUpAuth() + subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'initdb'), '-D', cls.data_path]) + + # Disable SSL verification for setup operations + env = dict(os.environ) + env['PGSSLMODE'] = 'disable' + + cls.server = subprocess.Popen([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'postgres'), '-D', + cls.data_path, '-c', + "config_file=%s" % cls.pg_conf], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + # Wait max 10 secs for the server to start + end = time.time() + 10 + while True: + line = cls.server.stderr.readline() + print(line) + if line.find(b"database system is ready to accept") != -1: + break + if time.time() > end: + raise Exception("Timeout connecting to PostgreSQL") + # Create a DB + subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'createdb'), '-h', 'localhost', '-p', cls.port, 'test_basic'], env=env) + # Inject test SQL from test path + test_sql = os.path.join(unitTestDataPath('provider'), 'testdata_pg.sql') + subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'psql'), '-h', 'localhost', '-p', cls.port, '-f', test_sql, cls.dbname], env=env) + # Create a role + subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'psql'), '-h', 'localhost', '-p', cls.port, '-c', 'CREATE ROLE "%s" WITH SUPERUSER LOGIN PASSWORD \'%s\'' % (cls.username, cls.password), cls.dbname], env=env) + + @classmethod + def tearDownClass(cls): + """Run after all tests""" + cls.server.terminate() + os.kill(cls.server.pid, signal.SIGABRT) + del cls.server + time.sleep(2) + rmtree(QGIS_AUTH_DB_DIR_PATH) + rmtree(cls.tempfolder) + + def setUp(self): + """Run before each test.""" + pass + + def tearDown(self): + """Run after each test.""" + pass + + @classmethod + def _getPostGISLayer(cls, type_name, layer_name=None, authcfg=''): + """ + PG layer factory + """ + if layer_name is None: + layer_name = 'pg_' + type_name + + # Warning: OGR needs the schema if it's not the default, so qgis_test.someData + connstring = "PG:dbname='%(dbname)s' host='%(hostname)s' port='%(port)s' sslmode='verify-full' sslrootcert='%(sslrootcert)s'%(authcfg)s|layername=qgis_test.someData" % ( + { + 'dbname': cls.dbname, + 'hostname': cls.hostname, + 'port': cls.port, + 'authcfg': ' authcfg=\'%s\'' % authcfg if authcfg else '', + 'sslrootcert': cls.sslrootcert_path, + } + ) + layer = QgsVectorLayer(connstring, layer_name, 'ogr') + return layer + + def testValidAuthAccess(self): + """ + Access the protected layer with valid credentials + """ + pg_layer = self._getPostGISLayer('testlayer_èé', authcfg=self.auth_config.id()) + self.assertTrue(pg_layer.isValid()) + + def testInvalidAuthAccess(self): + """ + Access the protected layer with not valid credentials + """ + pg_layer = self._getPostGISLayer('testlayer_èé') + self.assertFalse(pg_layer.isValid()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_authmanager_password_ows.py b/tests/src/python/test_authmanager_password_ows.py index 3b02d7ad998..36f11b38d6e 100644 --- a/tests/src/python/test_authmanager_password_ows.py +++ b/tests/src/python/test_authmanager_password_ows.py @@ -170,6 +170,19 @@ class TestAuthManager(unittest.TestCase): wms_layer = QgsRasterLayer(uri, layer_name, 'wms') return wms_layer + @classmethod + def _getGeoJsonLayer(cls, type_name, layer_name=None, authcfg=None): + """ + OGR layer factory + """ + if layer_name is None: + layer_name = 'geojson_' + type_name + uri = '%s://%s:%s/?MAP=%s&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=%s&VERSION=2.0.0&OUTPUTFORMAT=geojson' % (cls.protocol, cls.hostname, cls.port, cls.project_path, urllib.parse.quote(type_name)) + if authcfg is not None: + uri += " authcfg='%s'" % authcfg + geojson_layer = QgsVectorLayer(uri, layer_name, 'ogr') + return geojson_layer + def testValidAuthAccess(self): """ Access the HTTP Basic protected layer with valid credentials @@ -178,6 +191,8 @@ class TestAuthManager(unittest.TestCase): self.assertTrue(wfs_layer.isValid()) wms_layer = self._getWMSLayer('testlayer_èé', authcfg=self.auth_config.id()) self.assertTrue(wms_layer.isValid()) + geojson_layer = self._getGeoJsonLayer('testlayer_èé', authcfg=self.auth_config.id()) + self.assertTrue(geojson_layer.isValid()) def testInvalidAuthAccess(self): """ @@ -187,6 +202,8 @@ class TestAuthManager(unittest.TestCase): self.assertFalse(wfs_layer.isValid()) wms_layer = self._getWMSLayer('testlayer_èé') self.assertFalse(wms_layer.isValid()) + geojson_layer = self._getGeoJsonLayer('testlayer_èé') + self.assertFalse(geojson_layer.isValid()) def testInvalidAuthFileDownload(self): """