Skip to content

Commit

Permalink
Merge pull request #51287 from Djedouas/external-storage-aws-s3
Browse files Browse the repository at this point in the history
[ExternalStorage] AWS S3 cloud
  • Loading branch information
troopa81 committed Jan 5, 2023
2 parents d13a7b7 + 92771b7 commit c9453c4
Show file tree
Hide file tree
Showing 21 changed files with 1,052 additions and 84 deletions.
3 changes: 3 additions & 0 deletions .ci/run_tests.sh
Expand Up @@ -123,6 +123,9 @@ else
COMMAND=bash
fi

# Create an empty minio folder with appropriate permissions so www user can write inside it
mkdir -p /tmp/minio_tests/test_bucket && chmod -R 777 /tmp/minio_tests

# Create an empty webdav folder with appropriate permissions so www user can write inside it
mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests

Expand Down
10 changes: 10 additions & 0 deletions .docker/docker-compose-testing.yml
Expand Up @@ -18,6 +18,15 @@ services:
- ${QGIS_WORKSPACE}/.docker/webdav/passwords.list:/etc/nginx/.passwords.list
- /tmp/webdav_tests:/tmp/webdav_tests_root/webdav_tests

minio:
image: minio/minio
volumes:
- /tmp/minio_tests:/data
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=adminio€
command: server /data

qgis-deps:
tty: true
image: qgis3-build-deps-binary-image
Expand All @@ -27,6 +36,7 @@ services:
links:
# - mssql
- webdav
- minio
- httpbin
env_file:
- docker-variables.env
Expand Down
27 changes: 27 additions & 0 deletions .docker/docker-qgis-test.sh
Expand Up @@ -208,6 +208,33 @@ EOT

fi

#######################################
# Wait for Minio container to be ready
#######################################

if [ $# -eq 0 ] || [ $1 = "ALL_BUT_PROVIDERS" ] || [ $1 = "ALL" ] ; then

echo "::group::Setup Minio"

echo "Wait for minio to be ready..."
COUNT=0
while ! curl http://$QGIS_MINIO_HOST:$QGIS_MINIO_PORT &> /dev/null;
do
printf "."
sleep 5
if [[ $(( COUNT++ )) -eq 40 ]]; then
break
fi
done
if [[ ${COUNT} -eq 41 ]]; then
echo "Error: Minio docker timeout!!!"
else
echo "done"
fi

echo "::endgroup::"
fi

#######################################
# Wait for WebDAV container to be ready
#######################################
Expand Down
3 changes: 3 additions & 0 deletions .docker/docker-variables.env
Expand Up @@ -21,5 +21,8 @@ PUSH_TO_CDASH=false

XDG_RUNTIME_DIR=/tmp

QGIS_MINIO_HOST=minio
QGIS_MINIO_PORT=9000

QGIS_WEBDAV_HOST=webdav
QGIS_WEBDAV_PORT=80
1 change: 1 addition & 0 deletions .github/workflows/run-tests.yml
Expand Up @@ -396,6 +396,7 @@ jobs:
echo "TEST_BATCH=$TEST_BATCH"
echo "DOCKERFILE=$DOCKERFILE"
mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests
mkdir -p /tmp/minio_tests/test_bucket && chmod -R 777 /tmp/minio_tests
docker-compose -f .docker/$DOCKERFILE run qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH
- name: Archive test results report
Expand Down
1 change: 1 addition & 0 deletions src/auth/CMakeLists.txt
Expand Up @@ -19,6 +19,7 @@ add_subdirectory(pkipaths)
add_subdirectory(pkipkcs12)
add_subdirectory(apiheader)
add_subdirectory(maptiler_hmacsha256)
add_subdirectory(awss3)

if (WITH_OAUTH2_PLUGIN)
add_subdirectory(oauth2)
Expand Down
77 changes: 77 additions & 0 deletions src/auth/awss3/CMakeLists.txt
@@ -0,0 +1,77 @@
set(AUTH_AWSS3_SRCS
core/qgsauthawss3method.cpp
)

set(AUTH_AWSS3_HDRS
core/qgsauthawss3method.h
)

set(AUTH_AWSS3_UIS_H "")

if (WITH_GUI)
set(AUTH_AWSS3_SRCS ${AUTH_AWSS3_SRCS}
gui/qgsauthawss3edit.cpp
)
set(AUTH_AWSS3_HDRS ${AUTH_AWSS3_HDRS}
gui/qgsauthawss3edit.h
)
set(AUTH_AWSS3_UIS gui/qgsauthawss3edit.ui)
if (BUILD_WITH_QT6)
QT6_WRAP_UI(AUTH_AWSS3_UIS_H ${AUTH_AWSS3_UIS})
else()
QT5_WRAP_UI(AUTH_AWSS3_UIS_H ${AUTH_AWSS3_UIS})
endif()
endif()


# static library
add_library(authmethod_awss3_a STATIC ${AUTH_AWSS3_SRCS} ${AUTH_AWSS3_HDRS} ${AUTH_AWSS3_UIS_H})

target_include_directories(authmethod_awss3_a PUBLIC ${CMAKE_SOURCE_DIR}/src/auth/awss3/core)

# require c++17
target_compile_features(authmethod_awss3_a PRIVATE cxx_std_17)

target_link_libraries(authmethod_awss3_a qgis_core)

if (WITH_GUI)
target_include_directories(authmethod_awss3_a PRIVATE
${CMAKE_SOURCE_DIR}/src/auth/awss3/gui
${CMAKE_BINARY_DIR}/src/auth/awss3
)

target_link_libraries (authmethod_awss3_a qgis_gui)
endif()

target_compile_definitions(authmethod_awss3_a PRIVATE "-DQT_NO_FOREACH")



if (FORCE_STATIC_LIBS)
# for (external) mobile apps to be able to pick up provider for linking
install (TARGETS authmethod_awss3_a ARCHIVE DESTINATION ${QGIS_PLUGIN_DIR})
else()
# dynamically loaded module
add_library(authmethod_awss3 MODULE ${AUTH_AWSS3_SRCS} ${AUTH_AWSS3_HDRS} ${AUTH_AWSS3_UIS_H})

# require c++17
target_compile_features(authmethod_awss3 PRIVATE cxx_std_17)

target_link_libraries(authmethod_awss3 qgis_core)

if (WITH_GUI)
target_include_directories(authmethod_awss3 PRIVATE
${CMAKE_SOURCE_DIR}/src/auth/awss3/gui
${CMAKE_BINARY_DIR}/src/auth/awss3
)
target_link_libraries (authmethod_awss3 qgis_gui)
add_dependencies(authmethod_awss3 ui)
endif()

target_compile_definitions(authmethod_awss3 PRIVATE "-DQT_NO_FOREACH")

install (TARGETS authmethod_awss3
RUNTIME DESTINATION ${QGIS_PLUGIN_DIR}
LIBRARY DESTINATION ${QGIS_PLUGIN_DIR}
)
endif()
208 changes: 208 additions & 0 deletions src/auth/awss3/core/qgsauthawss3method.cpp
@@ -0,0 +1,208 @@
/***************************************************************************
qgsauthawss3method.cpp
--------------------------------------
Date : December 2022
Copyright : (C) 2022 by Jacky Volpes
Email : jacky dot volpes at oslandia dot com
***************************************************************************
* *
* 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. *
* *
***************************************************************************/

#include "qgsauthawss3method.h"

#include <QUrlQuery>
#include <QDateTime>
#include <QCryptographicHash>
#include <QMessageAuthenticationCode>

#include "qgsauthmanager.h"
#include "qgslogger.h"
#include "qgsapplication.h"

#ifdef HAVE_GUI
#include "qgsauthawss3edit.h"
#endif


const QString QgsAuthAwsS3Method::AUTH_METHOD_KEY = QStringLiteral( "AWSS3" );
const QString QgsAuthAwsS3Method::AUTH_METHOD_DESCRIPTION = QStringLiteral( "AWS S3" );
const QString QgsAuthAwsS3Method::AUTH_METHOD_DISPLAY_DESCRIPTION = tr( "AWS S3" );

QMap<QString, QgsAuthMethodConfig> QgsAuthAwsS3Method::sAuthConfigCache = QMap<QString, QgsAuthMethodConfig>();


QgsAuthAwsS3Method::QgsAuthAwsS3Method()
{
setVersion( 4 );
setExpansions( QgsAuthMethod::NetworkRequest );
setDataProviders( QStringList() << QStringLiteral( "awss3" ) );
}

QString QgsAuthAwsS3Method::key() const
{
return AUTH_METHOD_KEY;
}

QString QgsAuthAwsS3Method::description() const
{
return AUTH_METHOD_DESCRIPTION;
}

QString QgsAuthAwsS3Method::displayDescription() const
{
return AUTH_METHOD_DISPLAY_DESCRIPTION;
}

bool QgsAuthAwsS3Method::updateNetworkRequest( QNetworkRequest &request, const QString &authcfg,
const QString &dataprovider )
{
Q_UNUSED( dataprovider )
const QgsAuthMethodConfig config = getMethodConfig( authcfg );
if ( !config.isValid() )
{
QgsDebugMsg( QStringLiteral( "Update request config FAILED for authcfg: %1: config invalid" ).arg( authcfg ) );
return false;
}

const QByteArray username = config.config( QStringLiteral( "username" ) ).toLocal8Bit();
const QByteArray password = config.config( QStringLiteral( "password" ) ).toLocal8Bit();
const QByteArray region = config.config( QStringLiteral( "region" ) ).toLocal8Bit();

const QByteArray headerList = "host;x-amz-content-sha256;x-amz-date";
const QByteArray encryptionMethod = "AWS4-HMAC-SHA256";
const QDateTime currentDateTime = QDateTime::currentDateTime().toUTC();
const QByteArray date = currentDateTime.toString( "yyyyMMdd" ).toLocal8Bit();
const QByteArray dateTime = currentDateTime.toString( "yyyyMMddThhmmssZ" ).toLocal8Bit();

QByteArray canonicalPath = QUrl::toPercentEncoding( request.url().path(), "/" ); // Don't encode slash
if ( canonicalPath.isEmpty() )
{
canonicalPath = "/";
}

QByteArray method;
QByteArray payloadHash;
if ( request.hasRawHeader( "X-Amz-Content-SHA256" ) )
{
method = "PUT";
payloadHash = request.rawHeader( "X-Amz-Content-SHA256" );
}
else
{
method = "GET";
payloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // Sha256 of empty payload
request.setRawHeader( QByteArray( "X-Amz-Content-SHA256" ), payloadHash );
}

const QByteArray canonicalRequest = method + '\n' +
canonicalPath + '\n' +
'\n' +
"host:" + request.url().host().toLocal8Bit() + '\n' +
"x-amz-content-sha256:" + payloadHash + '\n' +
"x-amz-date:" + dateTime + '\n' +
'\n' +
headerList + '\n' +
payloadHash;

const QByteArray canonicalRequestHash = QCryptographicHash::hash( canonicalRequest, QCryptographicHash::Sha256 ).toHex();
const QByteArray stringToSign = encryptionMethod + '\n' +
dateTime + '\n' +
date + "/" + region + "/s3/aws4_request" + '\n' +
canonicalRequestHash;

const QByteArray signingKey = QMessageAuthenticationCode::hash( "aws4_request",
QMessageAuthenticationCode::hash( "s3",
QMessageAuthenticationCode::hash( region,
QMessageAuthenticationCode::hash( date, "AWS4" + password,
QCryptographicHash::Sha256 ),
QCryptographicHash::Sha256 ),
QCryptographicHash::Sha256 ),
QCryptographicHash::Sha256 );

const QByteArray signature = QMessageAuthenticationCode::hash( stringToSign, signingKey, QCryptographicHash::Sha256 ).toHex();

request.setRawHeader( QByteArray( "Host" ), request.url().host().toLocal8Bit() );
request.setRawHeader( QByteArray( "X-Amz-Date" ), dateTime );
request.setRawHeader( QByteArray( "Authorization" ),
encryptionMethod + "Credential=" + username + '/' + date + "/" + region + "/s3/aws4_request, SignedHeaders=" + headerList + ", Signature=" + signature );

return true;
}

void QgsAuthAwsS3Method::clearCachedConfig( const QString &authcfg )
{
removeMethodConfig( authcfg );
}

void QgsAuthAwsS3Method::updateMethodConfig( QgsAuthMethodConfig &mconfig )
{
Q_UNUSED( mconfig );
// NOTE: add updates as method version() increases due to config storage changes
}

QgsAuthMethodConfig QgsAuthAwsS3Method::getMethodConfig( const QString &authcfg, bool fullconfig )
{
const QMutexLocker locker( &mMutex );
QgsAuthMethodConfig config;

// check if it is cached
if ( sAuthConfigCache.contains( authcfg ) )
{
config = sAuthConfigCache.value( authcfg );
QgsDebugMsgLevel( QStringLiteral( "Retrieved config for authcfg: %1" ).arg( authcfg ), 2 );
return config;
}

// else build bundle
if ( !QgsApplication::authManager()->loadAuthenticationConfig( authcfg, config, fullconfig ) )
{
QgsDebugMsgLevel( QStringLiteral( "Retrieved config for authcfg: %1" ).arg( authcfg ), 2 );
return QgsAuthMethodConfig();
}

// cache bundle
putMethodConfig( authcfg, config );

return config;
}

void QgsAuthAwsS3Method::putMethodConfig( const QString &authcfg, const QgsAuthMethodConfig &mconfig )
{
const QMutexLocker locker( &mMutex );
QgsDebugMsgLevel( QStringLiteral( "Putting AWS S3 config for authcfg: %1" ).arg( authcfg ), 2 );
sAuthConfigCache.insert( authcfg, mconfig );
}

void QgsAuthAwsS3Method::removeMethodConfig( const QString &authcfg )
{
const QMutexLocker locker( &mMutex );
if ( sAuthConfigCache.contains( authcfg ) )
{
sAuthConfigCache.remove( authcfg );
QgsDebugMsgLevel( QStringLiteral( "Removed Aws S3 config for authcfg: %1" ).arg( authcfg ), 2 );
}
}

#ifdef HAVE_GUI
QWidget *QgsAuthAwsS3Method::editWidget( QWidget *parent ) const
{
return new QgsAuthAwsS3Edit( parent );
}
#endif

//////////////////////////////////////////////
// Plugin externals
//////////////////////////////////////////////


#ifndef HAVE_STATIC_PROVIDERS
QGISEXTERN QgsAuthMethodMetadata *authMethodMetadataFactory()
{
return new QgsAuthAwsS3MethodMetadata();
}
#endif

0 comments on commit c9453c4

Please sign in to comment.