From 2b9406e85afb9f0c60474154bcf6737ec3ad485c Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 19 Feb 2025 15:54:46 +0700 Subject: [PATCH] [oauth2] Allow for extra token(s) to be added into the header for header access methods --- src/auth/oauth2/core/qgsauthoauth2config.cpp | 15 ++ src/auth/oauth2/core/qgsauthoauth2config.h | 24 +++ src/auth/oauth2/core/qgsauthoauth2method.cpp | 15 ++ src/auth/oauth2/gui/qgsauthoauth2edit.cpp | 90 ++++++++++- src/auth/oauth2/gui/qgsauthoauth2edit.h | 11 ++ src/auth/oauth2/gui/qgsauthoauth2edit.ui | 148 ++++++++++++++++++- tests/src/auth/testqgsauthoauth2method.cpp | 10 ++ 7 files changed, 307 insertions(+), 6 deletions(-) diff --git a/src/auth/oauth2/core/qgsauthoauth2config.cpp b/src/auth/oauth2/core/qgsauthoauth2config.cpp index e73e47ec034..c5d2bba9c43 100644 --- a/src/auth/oauth2/core/qgsauthoauth2config.cpp +++ b/src/auth/oauth2/core/qgsauthoauth2config.cpp @@ -53,6 +53,7 @@ QgsAuthOAuth2Config::QgsAuthOAuth2Config( QObject *parent ) connect( this, &QgsAuthOAuth2Config::requestTimeoutChanged, this, &QgsAuthOAuth2Config::configChanged ); connect( this, &QgsAuthOAuth2Config::queryPairsChanged, this, &QgsAuthOAuth2Config::configChanged ); connect( this, &QgsAuthOAuth2Config::customHeaderChanged, this, &QgsAuthOAuth2Config::configChanged ); + connect( this, &QgsAuthOAuth2Config::extraTokensChanged, this, &QgsAuthOAuth2Config::configChanged ); // always recheck validity on any change // this, in turn, may emit validityChanged( bool ) @@ -230,6 +231,14 @@ void QgsAuthOAuth2Config::setCustomHeader( const QString &header ) emit customHeaderChanged( mCustomHeader ); } +void QgsAuthOAuth2Config::setExtraTokens( const QVariantMap &tokens ) +{ + const QVariantMap preval( mExtraTokens ); + mExtraTokens = tokens; + if ( preval != tokens ) + emit extraTokensChanged( mExtraTokens ); +} + void QgsAuthOAuth2Config::setRequestTimeout( int value ) { const int preval( mRequestTimeout ); @@ -269,6 +278,7 @@ void QgsAuthOAuth2Config::setToDefaults() setPersistToken( false ); setAccessMethod( QgsAuthOAuth2Config::AccessMethod::Header ); setCustomHeader( QString() ); + setExtraTokens( QVariantMap() ); setRequestTimeout( 30 ); // in seconds setQueryPairs( QVariantMap() ); } @@ -381,6 +391,9 @@ bool QgsAuthOAuth2Config::loadConfigTxt( if ( variantMap.contains( QStringLiteral( "customHeader" ) ) ) setCustomHeader( variantMap.value( QStringLiteral( "customHeader" ) ).toString() ); + if ( variantMap.contains( QStringLiteral( "extraTokens" ) ) ) + setExtraTokens( variantMap.value( QStringLiteral( "extraTokens" ) ).toMap() ); + if ( variantMap.contains( QStringLiteral( "requestTimeout" ) ) ) setRequestTimeout( variantMap.value( QStringLiteral( "requestTimeout" ) ).toInt() ); if ( variantMap.contains( QStringLiteral( "requestUrl" ) ) ) @@ -428,6 +441,7 @@ QByteArray QgsAuthOAuth2Config::saveConfigTxt( variant.insert( "clientSecret", clientSecret() ); variant.insert( "configType", static_cast( configType() ) ); variant.insert( "customHeader", customHeader() ); + variant.insert( "extraTokens", extraTokens() ); variant.insert( "description", description() ); variant.insert( "grantFlow", static_cast( grantFlow() ) ); variant.insert( "id", id() ); @@ -480,6 +494,7 @@ QVariantMap QgsAuthOAuth2Config::mappedProperties() const vmap.insert( QStringLiteral( "refreshTokenUrl" ), this->refreshTokenUrl() ); vmap.insert( QStringLiteral( "accessMethod" ), static_cast( this->accessMethod() ) ); vmap.insert( QStringLiteral( "customHeader" ), this->customHeader() ); + vmap.insert( QStringLiteral( "extraTokens" ), this->extraTokens() ); vmap.insert( QStringLiteral( "requestTimeout" ), this->requestTimeout() ); vmap.insert( QStringLiteral( "requestUrl" ), this->requestUrl() ); vmap.insert( QStringLiteral( "scope" ), this->scope() ); diff --git a/src/auth/oauth2/core/qgsauthoauth2config.h b/src/auth/oauth2/core/qgsauthoauth2config.h index 505d8009ce7..ced95a39813 100644 --- a/src/auth/oauth2/core/qgsauthoauth2config.h +++ b/src/auth/oauth2/core/qgsauthoauth2config.h @@ -92,6 +92,7 @@ class QgsAuthOAuth2Config : public QObject Q_PROPERTY( int requestTimeout READ requestTimeout WRITE setRequestTimeout NOTIFY requestTimeoutChanged ) Q_PROPERTY( QVariantMap queryPairs READ queryPairs WRITE setQueryPairs NOTIFY queryPairsChanged ) Q_PROPERTY( QString customHeader READ customHeader WRITE setCustomHeader NOTIFY customHeaderChanged ) + Q_PROPERTY( QVariantMap extraTokens READ extraTokens WRITE setExtraTokens NOTIFY extraTokensChanged ) //! Construct a QgsAuthOAuth2Config instance explicit QgsAuthOAuth2Config( QObject *parent = nullptr ); @@ -165,6 +166,14 @@ class QgsAuthOAuth2Config : public QObject */ QString customHeader() const { return mCustomHeader; } + /** + * Returns the extra tokens that will be added into the header for header access methods where + * the map key represents the token name and the associated value the header name to be used. + * + * \since QGIS 3.42 + */ + QVariantMap extraTokens() const { return mExtraTokens; } + //! Request timeout int requestTimeout() const { return mRequestTimeout; } @@ -316,6 +325,14 @@ class QgsAuthOAuth2Config : public QObject */ void setCustomHeader( const QString &header ); + /** + * Sets the extra \a tokens that will be added into the header for header access methods where + * the map key represents the token name and the associated value the header name to be used. + * + * \since QGIS 3.42 + */ + void setExtraTokens( const QVariantMap &tokens ); + //! Set request timeout to \a value void setRequestTimeout( int value ); //! Set query pairs to \a pairs @@ -378,6 +395,12 @@ class QgsAuthOAuth2Config : public QObject */ void customHeaderChanged( const QString & ); + /** + * Emitted when the extra tokens header list has changed + * \since QGIS 3.42 + */ + void extraTokensChanged( const QVariantMap & ); + //! Emitted when configuration request timeout has changed void requestTimeoutChanged( int ); //! Emitted when configuration query pair has changed @@ -407,6 +430,7 @@ class QgsAuthOAuth2Config : public QObject bool mPersistToken = false; AccessMethod mAccessMethod = AccessMethod::Header; QString mCustomHeader; + QVariantMap mExtraTokens; int mRequestTimeout = 30; // in seconds QVariantMap mQueryPairs; bool mValid = false; diff --git a/src/auth/oauth2/core/qgsauthoauth2method.cpp b/src/auth/oauth2/core/qgsauthoauth2method.cpp index 8e9f1326e08..7178d9eab38 100644 --- a/src/auth/oauth2/core/qgsauthoauth2method.cpp +++ b/src/auth/oauth2/core/qgsauthoauth2method.cpp @@ -322,6 +322,21 @@ bool QgsAuthOAuth2Method::updateNetworkRequest( QNetworkRequest &request, const { const QString header = o2->oauth2config()->customHeader().isEmpty() ? QString( O2_HTTP_AUTHORIZATION_HEADER ) : o2->oauth2config()->customHeader(); request.setRawHeader( header.toLatin1(), QStringLiteral( "Bearer %1" ).arg( o2->token() ).toLatin1() ); + + const QVariantMap extraTokens = o2->oauth2config()->extraTokens(); + if ( !extraTokens.isEmpty() ) + { + const QVariantMap receivedExtraTokens = o2->extraTokens(); + const QStringList extraTokenNames = extraTokens.keys(); + for ( const QString &extraTokenName : extraTokenNames ) + { + if ( receivedExtraTokens.contains( extraTokenName ) ) + { + request.setRawHeader( extraTokens[extraTokenName].toString().replace( '_', '-' ).toLatin1(), receivedExtraTokens[extraTokenName].toString().toLatin1() ); + } + } + } + #ifdef QGISDEBUG msg = QStringLiteral( "Updated request HEADER with access token for authcfg: %1" ).arg( authcfg ); QgsDebugMsgLevel( msg, 2 ); diff --git a/src/auth/oauth2/gui/qgsauthoauth2edit.cpp b/src/auth/oauth2/gui/qgsauthoauth2edit.cpp index 6d85b7fe2a7..ac2c0ba6f4f 100644 --- a/src/auth/oauth2/gui/qgsauthoauth2edit.cpp +++ b/src/auth/oauth2/gui/qgsauthoauth2edit.cpp @@ -191,6 +191,11 @@ void QgsAuthOAuth2Edit::setupConnections() connect( spnbxRequestTimeout, static_cast( &QSpinBox::valueChanged ), mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setRequestTimeout ); connect( mTokenHeaderLineEdit, &QLineEdit::textChanged, mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::setCustomHeader ); + connect( mExtraTokensTable, &QTableWidget::cellChanged, mOAuthConfigCustom.get(), [=]( int, int ) { + mOAuthConfigCustom->setExtraTokens( extraTokens() ); + } ); + connect( mAddExtraTokenButton, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::addExtraToken ); + connect( mRemoveExtraTokenButton, &QToolButton::clicked, this, &QgsAuthOAuth2Edit::removeExtraToken ); connect( mOAuthConfigCustom.get(), &QgsAuthOAuth2Config::validityChanged, this, &QgsAuthOAuth2Edit::configValidityChanged ); @@ -403,6 +408,8 @@ void QgsAuthOAuth2Edit::loadFromOAuthConfig( const QgsAuthOAuth2Config *config ) leApiKey->setText( config->apiKey() ); mTokenHeaderLineEdit->setText( config->customHeader() ); + populateExtraTokens( config->extraTokens() ); + // advanced chkbxTokenPersist->setChecked( config->persistToken() ); cmbbxAccessMethod->setCurrentIndex( static_cast( config->accessMethod() ) ); @@ -873,11 +880,19 @@ void QgsAuthOAuth2Edit::updateConfigAccessMethod( int indx ) case QgsAuthOAuth2Config::AccessMethod::Header: mTokenHeaderLineEdit->setVisible( true ); mTokenHeaderLabel->setVisible( true ); + mExtraTokensHeaderLabel->setVisible( true ); + mExtraTokensTable->setVisible( true ); + mAddExtraTokenButton->setVisible( true ); + mAddExtraTokenButton->setVisible( true ); break; case QgsAuthOAuth2Config::AccessMethod::Form: case QgsAuthOAuth2Config::AccessMethod::Query: mTokenHeaderLineEdit->setVisible( false ); mTokenHeaderLabel->setVisible( false ); + mExtraTokensHeaderLabel->setVisible( false ); + mExtraTokensTable->setVisible( false ); + mAddExtraTokenButton->setVisible( false ); + mAddExtraTokenButton->setVisible( false ); break; } } @@ -915,7 +930,6 @@ void QgsAuthOAuth2Edit::populateQueryPairs( const QVariantMap &querypairs, bool } } - void QgsAuthOAuth2Edit::queryTableSelectionChanged() { const bool hassel = tblwdgQueryPairs->selectedItems().count() > 0; @@ -941,7 +955,6 @@ QVariantMap QgsAuthOAuth2Edit::queryPairs() const return querypairs; } - void QgsAuthOAuth2Edit::addQueryPair() { addQueryPairRow( QString(), QString() ); @@ -956,7 +969,6 @@ void QgsAuthOAuth2Edit::removeQueryPair() tblwdgQueryPairs->removeRow( tblwdgQueryPairs->currentRow() ); } - void QgsAuthOAuth2Edit::clearQueryPairs() { for ( int i = tblwdgQueryPairs->rowCount(); i > 0; --i ) @@ -965,6 +977,78 @@ void QgsAuthOAuth2Edit::clearQueryPairs() } } +QVariantMap QgsAuthOAuth2Edit::extraTokens() const +{ + QVariantMap extraTokens; + for ( int i = 0; i < mExtraTokensTable->rowCount(); ++i ) + { + if ( mExtraTokensTable->item( i, 0 )->text().isEmpty() || mExtraTokensTable->item( i, 1 )->text().isEmpty() ) + { + continue; + } + extraTokens.insert( mExtraTokensTable->item( i, 0 )->text(), QVariant( mExtraTokensTable->item( i, 1 )->text() ) ); + } + return extraTokens; +} + +void QgsAuthOAuth2Edit::addExtraToken() +{ + mExtraTokensTable->blockSignals( true ); + addExtraTokenRow( QString(), QString() ); + mExtraTokensTable->blockSignals( false ); + + mExtraTokensTable->setFocus(); + mExtraTokensTable->setCurrentCell( mExtraTokensTable->rowCount() - 1, 0 ); + mExtraTokensTable->edit( mExtraTokensTable->currentIndex() ); +} + +void QgsAuthOAuth2Edit::removeExtraToken() +{ + mExtraTokensTable->removeRow( mExtraTokensTable->currentRow() ); +} + +void QgsAuthOAuth2Edit::clearExtraTokens() +{ + for ( int i = mExtraTokensTable->rowCount(); i > 0; --i ) + { + mExtraTokensTable->removeRow( i - 1 ); + } +} + +void QgsAuthOAuth2Edit::addExtraTokenRow( const QString &key, const QString &val ) +{ + const int rowCnt = mExtraTokensTable->rowCount(); + mExtraTokensTable->insertRow( rowCnt ); + + const Qt::ItemFlags itmFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable + | Qt::ItemIsEditable | Qt::ItemIsDropEnabled; + + QTableWidgetItem *keyItm = new QTableWidgetItem( key ); + keyItm->setFlags( itmFlags ); + mExtraTokensTable->setItem( rowCnt, 0, keyItm ); + + QTableWidgetItem *valItm = new QTableWidgetItem( val ); + keyItm->setFlags( itmFlags ); + mExtraTokensTable->setItem( rowCnt, 1, valItm ); +} + +void QgsAuthOAuth2Edit::populateExtraTokens( const QVariantMap &tokens, bool append ) +{ + mExtraTokensTable->blockSignals( true ); + if ( !append ) + { + clearExtraTokens(); + } + + QVariantMap::const_iterator i = tokens.constBegin(); + while ( i != tokens.constEnd() ) + { + addExtraTokenRow( i.key(), i.value().toString() ); + ++i; + } + mExtraTokensTable->blockSignals( false ); +} + void QgsAuthOAuth2Edit::parseSoftwareStatement( const QString &path ) { QFile file( path ); diff --git a/src/auth/oauth2/gui/qgsauthoauth2edit.h b/src/auth/oauth2/gui/qgsauthoauth2edit.h index b5f8a2e65c3..738d6620803 100644 --- a/src/auth/oauth2/gui/qgsauthoauth2edit.h +++ b/src/auth/oauth2/gui/qgsauthoauth2edit.h @@ -90,6 +90,14 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi void populateQueryPairs( const QVariantMap &querypairs, bool append = false ); + void addExtraToken(); + + void removeExtraToken(); + + void clearExtraTokens(); + + void populateExtraTokens( const QVariantMap &tokens, bool append = false ); + void queryTableSelectionChanged(); void updateConfigQueryPairs(); @@ -139,6 +147,9 @@ class QgsAuthOAuth2Edit : public QgsAuthMethodEdit, private Ui::QgsAuthOAuth2Edi void addQueryPairRow( const QString &key, const QString &val ); QVariantMap queryPairs() const; + void addExtraTokenRow( const QString &key, const QString &val ); + QVariantMap extraTokens() const; + int customTab() const { return 0; } int definedTab() const { return 1; } int statementTab() const { return 2; } diff --git a/src/auth/oauth2/gui/qgsauthoauth2edit.ui b/src/auth/oauth2/gui/qgsauthoauth2edit.ui index 4e415f99068..c1588ba5bc7 100644 --- a/src/auth/oauth2/gui/qgsauthoauth2edit.ui +++ b/src/auth/oauth2/gui/qgsauthoauth2edit.ui @@ -253,7 +253,7 @@ - + Qt::Vertical @@ -366,7 +366,7 @@ - + @@ -523,7 +523,7 @@ - + @@ -637,6 +637,146 @@ + + + + + 0 + 0 + + + + Extra token(s) header + + + true + + + + + + + 3 + + + + + + 1 + 0 + + + + + 50 + 70 + + + + QAbstractItemView::AllEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + QAbstractItemView::SingleSelection + + + true + + + false + + + 120 + + + 120 + + + true + + + true + + + false + + + + Token + + + + + Header name + + + + These extra tokens will be included in the request headers sent to the resource when provided by the token endpoint + + + + + + + + + + 24 + 0 + + + + + 75 + true + + + + + + + + + + + + + 24 + 0 + + + + + 75 + true + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + @@ -892,6 +1032,7 @@ 6 + 3 @@ -999,6 +1140,7 @@ + diff --git a/tests/src/auth/testqgsauthoauth2method.cpp b/tests/src/auth/testqgsauthoauth2method.cpp index 89caee3d1b5..4b04dc27426 100644 --- a/tests/src/auth/testqgsauthoauth2method.cpp +++ b/tests/src/auth/testqgsauthoauth2method.cpp @@ -125,6 +125,9 @@ QgsAuthOAuth2Config *TestQgsAuthOAuth2Method::baseConfig( bool loaded ) config->setAccessMethod( QgsAuthOAuth2Config::AccessMethod::Header ); config->setCustomHeader( QStringLiteral( "x-auth" ) ); config->setRequestTimeout( 30 ); // in seconds + QVariantMap extraTokens; + extraTokens.insert( "id_token", "X-QGS-OPENID" ); + config->setExtraTokens( extraTokens ); QVariantMap queryPairs; queryPairs.insert( "pf.username", "myusername" ); queryPairs.insert( "pf.password", "mypassword" ); @@ -147,6 +150,9 @@ QByteArray TestQgsAuthOAuth2Method::baseConfigTxt( bool pretty ) " \"configType\": 1,\n" " \"customHeader\": \"x-auth\",\n" " \"description\": \"A test config\",\n" + " \"extraTokens\": {\n" + " \"id_token\": \"X-QGS-OPENID\"\n" + " },\n" " \"grantFlow\": 0,\n" " \"id\": \"abc1234\",\n" " \"name\": \"MyConfig\",\n" @@ -178,6 +184,7 @@ QByteArray TestQgsAuthOAuth2Method::baseConfigTxt( bool pretty ) "\"configType\":1," "\"customHeader\":\"x-auth\"," "\"description\":\"A test config\"," + "\"extraTokens\":{\"id_token\":\"X-QGS-OPENID\"}," "\"grantFlow\":0," "\"id\":\"abc1234\"," "\"name\":\"MyConfig\"," @@ -215,6 +222,9 @@ QVariantMap TestQgsAuthOAuth2Method::baseVariantMap() vmap.insert( "password", "mypassword" ); vmap.insert( "persistToken", false ); vmap.insert( "customHeader", "x-auth" ); + QVariantMap extraTokens; + extraTokens.insert( "id_token", "X-QGS-OPENID" ); + vmap.insert( "extraTokens", extraTokens ); QVariantMap qpairs; qpairs.insert( "pf.password", "mypassword" ); qpairs.insert( "pf.username", "myusername" );